第五章、原型
JavaScript中的对象有一个特殊的[[Prototype]]内置属性, 其实就是对于其他对象的引用。几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
Object.prototype
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype。由于所有的“ 普通”(内置, 不是特定主机的扩展)对象都“ 源于”(或者说把[[Prototype]]链的顶端设置为)这个Object.prototype对象,所以它包含JavaScript中许多通用的功能。
Object.create() 建立两个对象之间的连接
const parentObj = {
name: 'parent'
}
const obj = Object.create(parentObj)
obj.name = 'obj'
属性的设置和屏蔽
上段代码中,执行 obj.name = 'obj' 时,会出现以下情况:
- parentObj作为obj的隐式原型,存在 name 属性,并且没有标记为只读(writable: false),那么会直接在 obj 中添加一个名为 name 的新属性,即屏蔽属性。
const parentObj = {
name: 'parent'
}
const obj = Object.create(parentObj)
obj.name = 'obj'
- parentObj作为obj的隐式原型,存在 name 属性,并且标记为只读(writable: false),那么无法在obj上添加新属性,赋值语句会被忽略,严格模式下会抛出错误。总之,不会产生屏蔽属性。
const parentObj = {}
Object.defineProperty(parentObj, 'name', {
value: 'parent',
writable: false,
})
const obj = Object.create(parentObj)
obj.name = 'obj'
console.log(obj)
- parentObj作为obj的隐式原型,存在 name 属性,并且它是一个setter,,那就一定会调用这个setter。不会在obj中添加或修改属性。
const parentObj = {}
Object.defineProperty(parentObj, 'name', {
set(val) {
console.log(val)
},
})
const obj = Object.create(parentObj)
obj.name = 'obj'
原型
“类”
这个对象是在调用new Foo()时创建的,最后会被(有点武断地)关联到这个“Foo点prototype”对象上。
一些观点:
在面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样。之所以会这样是因为实例化(或者继承)一个类就意味着“ 把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。
但是在JavaScript中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建多个对象,它们[[Prototype]]关联的是同一个对象。但是在默认情况下并不会进行复制,因此这些对象之间并不会完全失去联系,它们是互相关联的。
new Foo()会生成一个新对象(我们称之为a),这个新对象的内部链接[[Prototype]]关联的是Foo.prototype对象。
最后我们得到了两个对象, 它们之间互相关联, 就是这样。 我们并没有初始化一个类, 实际上我们并没有从“类”中复制任何行为到一个对象中,只是让两个对象互相关联。
实际上,绝大多数JavaScript开发者不知道的秘密是,new Foo()这个函数调用实际上并没有直接创建关联, 这个关联只是一个意外的副作用。new Foo()只是间接完成了我们的目标:一个关联到其他对象的新对象。
JavaScript中并没有类的概念,所有这些关于类的说法都是荒谬的,不准确的。原型归根到底是建立两个对象之间的连接,这才是js本质上的东西。
作者的看法:
因此我认为这个容易混淆的组合术语“ 原型继承”(以及使用其他面向类的术语比如“类”、“构造函数”、“实例”、“多态”,等等)严重影响了大家对于JavaScript机制真实原理的理解。
继承意味着复制操作,JavaScript(默认)并不会复制对象属性。 相反,JavaScript会在两个对象之间创建一个关联, 这样一个对象就可以通过委托访问另一个对象的属性和函数。委托(参见第6章)这个术语可以更加准确地描述JavaScript中对象的关联机制。
构造函数
function Foo() {}
const foo = new Foo()
foo.constructor // Foo
/**
* 实际上foo本身并没有.constructor属性。
* 而且,虽然foo.constructor确实指向Foo函数,但是这个属性并不是表示a由Foo“构造”。
* 实际上,.constructor引用同样被委托给了Foo.prototype,而Foo.prototype.constructor默认指向Foo。
*/
构造函数vs普通函数
实际上,Foo和你程序中的其他函数没有任何区别。 函数本身并不是构造函数, 然而, 当你在普通的函数调用前面加上new关键字之后, 就会把这个函数调用变成一个“ 构造函数调用” 。实际上,new会劫持所有普通函数并用构造对象的形式来调用它。
换句话说,在JavaScript中对于“构造函数”最准确的解释是,所有带new的函数调用。
函数不是构造函数,但是当且仅当使用new时,函数调用会变成“构造函数调用”。
如何建立两个对象之间的连接
- es5 Object.create(source)
const foo = {
name: 'foo',
getName: function () {
console.log(this.name)
}
}
const bar = Object.create(foo)
bar.name = 'bar'
bar.getName()
- es6 Object.setPrototypeOf(target, source)
const foo = {
name: 'foo',
getName: function () {
console.log(this.name)
}
}
const bar = {
name: 'bar'
}
Object.setPrototypeOf(bar, foo)
bar.getName()
检查“类”之间的关系
instanceof
obj instanceof Foo
左边是一个对象,右边是一个函数
实现的意义:在a的整条[[Prototype]]链中是否有指向Foo.prototype的对象
- 这个方法只能处理对象(a)和函数( 带.prototype引用的Foo)之间的关系。 如果你想判断两个对象(比如a和b)之间是否通过[[Prototype]]链关联, 只用instanceof无法实现。*
对象关联
原型链:如果在对象上没有找到需要的属性或者方法引用,引擎就会继续在[[Prototype]]关联的对象上进行查找。同理,如果在后者中也没有找到需要的引用就会继续查找它的[[Prototype]],以此类推。这一系列对象的链接被称为“原型链”。
Object.create()
Object.create(..)会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥[[Prototype]]机制的威力( 委托)并且避免不必要的麻烦( 比如使用new的构造函数调用会生成.prototype和.constructor引用)
ployfill
if (!Object.create) {
Object.create = function (obj) {
function Foo() {}
Foo.prototype = obj
return new Foo()
}
}