在 Javascript 里,所有的数据类型都是「对象」(object),这一点与 Java 非常相似,所以很多人都会误以为 JavaScript 是一门面向对象的语言。
但从严格意义上来讲,Javascript 并不是一门面向对象语言,而是基于对象,也可以说是基于原型。Javascript 并没有「类」(class) 的概念,也没有提供像抽象、继承、重载等有关面向对象语言的许多特性。那么,Javascript 是如何通过 object 来实现 class 类似表现的?
对象原型
可以说 JavaScript 既是一个面向对象的语言又是一个面向过程的语言。
在 JavaScript 中,通过在运行时,给空对象附加方法和属性来创建对象,与编译语言如 C++ 和 Java 中常见的通过语法来定义类相反。对象构造后,它可以用作是创建相似对象的原型。
前面我们有说到,Javascript 里面所有的数据类型都是对象。在这其中,每个对象上都有 __proto__
和 constructor
这两个属性。
1 | const array = [114, 514]; |
函数也是对象,较为特殊的是,除了 __proto__
和 constructor
,函数还有 prototype
这个独有属性。prototype
是显式原型,__proto__
是隐式原型。
1 | // 等价于 var foo = new Function(); |
原型链
对象的 __proto__
属性,是在创建对象时自动添加的,默认值为其构造函数的 prototype
。函数的 prototype
属性是定义时自动添加的,默认为 {}
空对象,一层一层的 __proto__
便形成了原型链。
1 | function Foo() {} |
prototype
的作用,就是包含可以由特定类型的所有实例共享的属性和方法,也就是让该函数所实例化的对象们,都可以找到公用的属性和方法。当我们试图去访问一个对象上的属性时,如果不存在,便会顺着原型链一直往下找,直到找到为止。
所以,我们可以在字符串上调用 replace()
、trim()
,能在数组上调用 join()
、push()
,正是因为顺着原型链找到了对应的方法。如果比较熟悉链表 (linked list),那么理解起来会特别快。
值得一提的是,Object
的原型是 null
,因为它是原型链的最顶端。
1 | console.log(Object.prototype.__proto__ === null); // -> true |
[[prototype]]
与 __proto__
前面我们有说到,所有对象都有 __proto__
属性,可是在浏览器里打印却显示的是 [[prototype]]
而非 __proto__
。
其实 [[prototype]]
和 __proto__
意义相同,均表示对象的内部属性,其值指向对象原型。前者在一些书籍、规范中表示一个对象的原型属性,后者则是在浏览器实现中指向对象原型。
__proto__
并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性。虽然目前很多现代浏览器的 JavaScript 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,建议只使用 Object.getPrototypeOf()
方法来获取实例对象的原型。
构造函数
constructor
属性也是对象才拥有的,它是从一个对象指向一个函数,含义就是指向该对象的构造函数。
每个对象都有构造函数,不过 Function
对象比较特殊,它的构造函数就是它自己(因为 Function
可以看成是一个函数,也可以是一个对象),所有函数最终都是由 Function()
构造函数得来,所以 constructor
属性的指向就是 Function()
。
ES6
在 ES6 中,新引入了 class
关键字,让定义类更接近传统写法,不过其本质也只是构造函数的另一种写法,最后还是绕不开原型链。
有兴趣你可以查阅 深入浅出 function 与 class 一栏。