搞懂原型链
4月 11, 2022
与一般基于类的面向对象语言(C++、Java)不同,JavaScript 是基于原型的语言。
原型 [[Prototype]]
#
打开浏览器的控制台,输入 let b = {"hello": "world"}
,然后查看 b
,你会发现一个 [[Prototype]]
属性。这是每个对象都有的隐藏属性,它要么为 null
,要么是对另一个对象的引用,那个对象就被称为 b
的 原型(proto type)。
当我们从一个对象中读取一个缺失的属性时,JavaScript 会自动从对象的原型中获取该属性,如果原型上也没有该属性,就依次层层向上搜索,直到找到属性或者遇到原型对象为 null,即原型链的尾端。这种属性查找的方式被称为原型链(prototype chain)。
虽然有这个属性,但是你却没有办法通过 b.[[Prototpye]]
进行使用,因为它是隐式的,只能在文档中用于表示指向 b
的原型,而在代码中要通过 b.__proto__
进行显式访问。
非标准的 __proto__
#
[[Prototpye]]
与 __proto__
并不是等价关系,从上图也可以看出,__proto__
只是 b.[[Prototype]]
的 getter/setter。然而 __proto__
被认为是一种非标准的访问 [[Prototype]]
的方式,从 ES6 开始,可以通过 Object.getPrototypeOf()
和 Object.setPrototypeOf()
来访问 b.[[Prototype]]
。
还有需要注意的是,当使用 for .. in ..
遍历对象属性的时候,也会将其继承自其他对象的属性一并遍历。而通过 Object.keys()
得到的属性只有对象自己的属性。还可以通过 obj.hasOwnProperty(prop)
来判断属性是自己的还是继承的。
函数的 prototype 属性 #
现在还要介绍一种名为 prototype
(不是 [[Prototype]]
)的常规属性,这就开始有点乱了,所以请先耐心看完再思考。
__proto__
是每个对象都有的一个属性,而 prototype
是函数才会有的属性。JavaScript 的每个函数都有一个 prototype
属性,默认的 prototype
是一个包含 constructor
属性的对象,并且 constructor
指向了自身。
let f = function(){}
f.prototype //{constructor: f}
let a = new f()
a.__proto__ //{constructor: f}
默认情况下,JavaScript 在 new F()
时通过访问一个函数的 F.prototpye.constructor
来获取一个对象的构造函数,然后生成新的对象。这样的话,所有从 new f()
得到的对象,都继承了 F.prototype
,并将其引用赋值给 [[Prototype]]
。
a.__proto__ === f.prototype // true
这应该也一定程度解释了 Object
的类型也需要是 function
。而 new
和 class
只是一个模仿类思想的语法糖。
typeof Object //'function'
你可能听说过 Object.create()
方法,new
和 Object.create()
不一样。new
就使用 f()
作为构造函数,创建的对象包含了 f()
的所有属性和方法,即实现了继承,而 Object.create(proto, propertiesObject)
的参数可以另外指定新对象的 [[Prototype]]
和其它 properties
,未指定的东西就不会获得,没有继承的概念,更像是拷贝一个恢复了出厂设置的自己。如果 proto
传 null
,创建的对象就没有原型。
let animal = {
eats: true
};
let rabbit = Object.create(animal);
let p = Object.create(null);
很多内建对象比如 Function、Array、Map 等都实现了自己的 prototype,不是只有单纯的 constructor
属性,我们绝大部分对内建类型的数据进行操作的方法基本都来自其 prototype
属性中的实现。