JavaScript的面向对象设计-1

为什么JS没有Class?Prototype又是什么?

Posted by Chase Gu on September 2, 2020

JavaScript的面向对象设计-1

阮一峰老师的三篇文章:

Javascript继承机制的设计思想

Javascript 面向对象编程(一):封装

Javascript面向对象编程(二):构造函数的继承

“类”与对象

Javascript是一种基于对象(object-based)的语言,你遇到的所有东西几乎都是对象。但是,它又不是一种真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)。

它没有”子类”和”父类”的概念,也没有”类”(class)和”实例”(instance)的区分,全靠一种很奇特的”原型链”(prototype chain)模式,来实现继承。

为什么没有class?

为了解决浏览器的效率问题,网景公司急需一种与浏览器网页互动的脚本语言。JavaScript的开发者Brendan Eich鉴于当时的需求,认为没有必要将这门语言设计得很复杂,只需要完成一些简单的操作就行了。

当时正是面向对象思想最兴盛的时期,所以Brendan Eich将JS中所有数据类型都设置为对象,但是如果再加上和C++一样的继承特性,就太过于复杂了,超出了当时的需求。于是Brendan Eich使用了自己的方案。

没有class怎么new?

C++和Java的new后面都跟上class名,JS没有class,于是便使用构造函数来代替class。

1
2
3
4
function Cat(name) {
    this.name = name;
}
let cat1 = new Cat("Cat1");

如何将同一class的对象联系在一起?

生成对象有很多方法,直接量就是其中一种。我们可以不通过new来创建对象。

所以我们可以通过一个特定的规格,来生成一些列对象。这个特定的规格就像是class一样,表示一类。

1
2
3
4
let cat1 = {};
cat1.name = "Cat1";
let cat2 = {};
cat2.name = "Cat2";

我们可以写一个函数来解决代码重复的问题

1
2
3
4
5
function Cat(name) {
    return {
        name: name
    }
}

这种方法有一个弊端:没有表现出cat1和cat2的关系,他们应该是同一类,即都是猫

于是我们可以使用构造函数模式

为了解决从原型对象生成实例的问题,Javascript提供了一个构造函数(Constructor)模式。

所谓”构造函数”,其实就是一个普通函数,但是内部使用了this变量。对构造函数使用new运算符,就能生成实例,并且this变量会绑定在实例对象上。

1
2
3
4
5
function Cat(name) {
    this.name = name;
}
let cat1 = new Cat("Cat1");
let cat2 = new Cat("Cat2");

这时cat1和cat2会自动含有一个constructor属性,指向它们的构造函数(准确地说,是cat1和cat2的__proto__指向的对象有constructor属性,关于原型对象下面再做介绍)。这样就将这两个对象联系在一起。

为什么需要原型对象?

上面创建方式存在浪费内存的问题

下面的代码中,eat函数对于每个Cat都一样,但是每个Cat的实例却没有共享这个函数,而是每个实例创建一个

1
2
3
4
5
6
7
8
9
function Cat(name) {
    this.name = name;
    this.eat = function() {
        console.log("eat");
    }
}
let cat1 = new Cat("Cat1");
let cat2 = new Cat("Cat2");
console.log(cat1.eat == cat2.eat()); // false

如果使用C++或者Java,我们可以设置函数、属性为静态(static)。这样函数、属性就变为类所有的,所有该类的实例都共用一个函数、属性。

在JS中,即使用原型(prototype)来实现。创建一个原型对象,Cat构造函数有一个prototype属性来指向这个原型对象,Cat的实例直接调用和访问该原型对象的函数和属性。

在原型对象中设置相应的函数和属性,这样Cat的所有实例都共用这一个原型对象中的函数和属性了。

1
2
3
4
5
6
7
8
9
function Cat(name) {
    this.name = name;
}
Cat.prototype.eat = function() {
    console.log("eat");
}
let cat1 = new Cat("Cat1");
let cat2 = new Cat("Cat2");
console.log(cat1.eat == cat2.eat()); // true

访问原型对象有三种方法

  • obj.__proto__
  • obj.constructor.prototype
  • Object.getPrototypeOf(obj)

其中在实际开发中并不推荐使用obj.__proto__,最建议使用的是Object.getPrototypeOf(obj)