共计 9844 个字符,预计需要花费 25 分钟才能阅读完成。
本文是我学习《你所不晓得的 javaScript 上卷》的读书笔记的整顿。
更多具体内容,请 微信搜寻“前端爱好者
“, 戳我 查看。
原型
1. [[Prototype]]
JavaScript 中的对象有一个非凡的 [[Prototype]] 内置属性: 对于其余对象的援用。
简直所有
的对象在创立时 [[Prototype]] 属性都会被赋予一个非空的值。
1.1 [[Prototype]] 援用有什么用
当你试图援用对象的属性时会触发 [[Get]] 操作。
对于默认的 [[Get]] 操作来说,第一步是查看对象自身是否有这个属性,如果有的话就应用它。
然而如果 属性 不在 对象自身 中,就须要应用对象的 [[Prototype]] 链了。
var anotherObject = {a:2};
// 创立一个关联到 anotherObject 的对象
var myObject = Object.create(anotherObject);
myObject.a; // 2
myObject 对象的 [[Prototype]] 关联到了 anotherObject。显然 myObject.a 并不存在,然而尽管如此,属性拜访依然胜利地(在 anotherObject 中)找到了值 2。
然而,如果 anotherObject 中也找不到 a 并且 [[Prototype]] 链不为空的话,就会持续查找上来。
这个过程会继续到找到匹配的属性名或者查找残缺条 [[Prototype]] 链。
for..in – 同样会查找对象的整条原型链
应用 for..in 遍历对象时原理和查找 [[Prototype]] 链相似,任何能够通过原型链拜访到
(并且是 enumerable)的属性都会被枚举。
应用 in 操作符来查看属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)
1.2 [[Prototype]] 的“止境”
所有一般的 [[Prototype]] 链最终都会指向内置的 Object.prototype
。
因为所有的“一般”(内置)对象都“源于”这个 Object.prototype 对象,所以它蕴含 JavaScript 中许多通用的性能。
var ss = {}
ss ==>
{}
__proto__:
constructor:
ƒ Object()
hasOwnProperty:
ƒ hasOwnProperty()
isPrototypeOf:
ƒ isPrototypeOf()
propertyIsEnumerable:
ƒ propertyIsEnumerable()
toLocaleString:
ƒ toLocaleString()
toString:
ƒ toString()
valueOf:
ƒ valueOf()
__defineGetter__:
ƒ __defineGetter__()
__defineSetter__:
ƒ __defineSetter__()
__lookupGetter__:
ƒ __lookupGetter__()
__lookupSetter__:
ƒ __lookupSetter__()
get __proto__:
ƒ __proto__()
set __proto__:
ƒ __proto__()
1.3 属性设置和屏蔽
一个对象设置属性的过程:
1myObject.foo = "bar";
- 如果 myObject 对象中蕴含名为 foo 的一般数据拜访属性,这条赋值语句只会批改已有的属性值。
- 如果 foo 不是间接存在于 myObject 中,[[Prototype]] 链就会被遍历,相似 [[Get]] 操作。
如果原型链上找不到 foo
,foo 就会被间接增加到 myObject 上。- 如果属性名 foo 既呈现在 myObject 中也呈现在 myObject 的 [[Prototype]] 链下层,那么就会
产生屏蔽
。
myObject 中蕴含的 foo 属性会屏蔽原型链下层的所有 foo 属性,因为
myObject.foo 总是会抉择原型链中最底层的 foo 属性。
如果 foo 不间接存在于 myObject 中而是存在于原型链下层时 myObject.foo = “bar” 会呈现的三种状况。
- 如果在 [[Prototype]] 链下层存在名为 foo 的一般数据拜访属性()并且没有被标记为只读(writable:false),那就会间接在 myObject 中增加一个名为 foo 的新属性,它是屏蔽属性。
- 如果在 [[Prototype]] 链下层存在 foo,然而它被标记为只读(writable:false),那么无奈批改已有属性或者在 myObject 上创立屏蔽属性。
如果运行在严格模式下,代码会抛出一个谬误。否则,这条赋值语句会被疏忽。总之,不会产生屏蔽。 - 如果在 [[Prototype]] 链下层存在 foo 并且它是一个 setter(),那就肯定会调用这个 setter。
foo 不会被增加到(或者说屏蔽于)myObject,也不会从新定义 foo 这个 setter。
大多数开发者都认为如果向 [[Prototype]] 链下层曾经存在的属性([[Put]])赋值,就肯定会触发屏蔽,然而如你所见,三种状况中只有一种(第一种)是这样的。
如果你心愿在第二种和第三种状况下也屏蔽 foo,那就不能应用 = 操作符来赋值,而是应用 Object.defineProperty(..)
来向 myObject 增加 foo。
var anotherObject = {a:2};
var myObject = Object.create(anotherObject);
anotherObject.a; // 2
myObject.a; // 2
anotherObject.hasOwnProperty("a"); // true
myObject.hasOwnProperty("a"); // false
myObject.a++; // 隐式屏蔽!anotherObject.a; // 2
myObject.a; // 3
myObject.hasOwnProperty("a"); // true
只管 myObject.a++ 看起来应该(通过委托)查找并减少 anotherObject.a 属性,然而别忘了 ++ 操作相当于 myObject.a = myObject.a + 1。
因而 ++ 操作首先会通过 [[Prototype]] 查找属性 a 并从 anotherObject.a 获取以后属性值 2,而后给这个值加 1,接着用 [[Put]] 将值 3 赋给 myObject 中新建的屏蔽属性 a,天呐!
批改委托属性时肯定要小心。如果想让 anotherObject.a 的值减少,惟一的方法是 anotherObject.a++。
2.“类”
为什么一个对象须要关联到另一个对象?这样做有什么益处?
JavaScript 和面向类的语言不同,它并没有类来作为对象的形象模式或者说蓝图。
JavaScript 中只有对象。
实际上,JavaScript 才是真正应该被称为“面向对象”的语言,因为它是少有的能够不通过类,间接创建对象的语言。
在 JavaScript 中,类无奈形容对象的行为,(因为基本就不存在类!)对象间接定义本人的行为。
2.1“类”函数
多年以来,JavaScript 中有一种奇怪的行为始终在被无耻地滥用,那就是模拟类。
这种奇怪的“相似类”的行为利用了 函数
的一种非凡个性
:
所有的函数默认都会领有一个名为 prototype 的私有并且不可枚举的属性,它会指向另一个对象:
function Foo() {// ...}
Foo.prototype; // { }
这个对象通常被称为 Foo 的原型,因为咱们通过名为 Foo.prototype 的属性援用来拜访它。
抛开名字不谈,这个对象到底是什么?
这个对象是在调用 new Foo()时创立的,最初会被
(有点果断地)关联到这个“Foo 点 prototype”对象上。
function Foo() {// ...}
var a = new Foo();
Object.getPrototypeOf(a) === Foo.prototype; // true
Object.getPrototypeOf() 办法返回指定对象的原型(外部 [[Prototype]] 属性的值)。
在面向类的语言中,类能够被复制(或者说实例化)屡次,就像用模具制作货色一样。
然而在 JavaScript 中,并没有相似的复制机制。你不能创立一个类的多个实例,只能创立多个对象,它们 [[Prototype]] 关联的是同一个对象。
然而在默认状况下并不会进行复制,因而这些对象之间并不会齐全失去分割,它们是相互关联的。
留神:
new Foo() 会生成一个新对象(咱们称之为 a),这个新对象的外部链接 [[Prototype]] 关联的是 Foo.prototype 对象。
最初咱们失去了两个对象,它们之间相互关联,就是这样。咱们并没有初始化一个类,实际上咱们并没有从“类”中复制任何行为到一个对象中,只是 让两个对象相互关联
。
对于名称
在 JavaScript 中,咱们并不会将一个对象(“类”)复制到另一个对象(“实例”),只是将它们关联起来。从视觉角度来说,[[Prototype]] 机制如下图所示,箭头从右到左,从下到上:
这个机制通常被称为 原型继承
。
2.2“构造函数”
function Foo() {// ...}
var a = new Foo();
到底是什么让咱们认为 Foo 是一个“类”呢?
- 其中一个起因是咱们看到了关键字 new,在面向类的语言中结构类实例时也会用到它。
- 另一个起因是,看起来咱们执行了类的构造函数办法,Foo() 的调用形式很像初始化类时类构造函数的调用形式。
function Foo() {// …}
Foo.prototype.constructor === Foo; // true
var a = new Foo();
a.constructor === Foo; // true
Foo.prototype 默认(在代码中第一行申明时!)有一个私有并且不可枚举 的属性 .constructor,这个属性援用的是 对象关联的函数
(本例中是 Foo)。
此外,咱们能够看到通过“构造函数”调用 new Foo() 创立的对象
也有一个 .constructor 属性,指向“创立这个对象的函数”。
构造函数还是调用
function Foo() {// ...}
var a = new Foo();
这段代码很容易让人认为 Foo 是一个构造函数,因为咱们应用 new 来调用它并且看到它“结构”了一个对象。
实际上,Foo 和你程序中的其余函数没有任何区别。
函数自身并不是构造函数,
然而,当你在一般的函数调用后面加上 new 关键字之后,就会把这个函数调用变成一个“结构函数调用”。
实际上,new 会 劫持所有一般函数并用结构对象的模式来调用它
。
在 JavaScript 中对于“构造函数”最精确的解释是,
所有带 new 的函数调用
。
函数不是构造函数,然而当且仅当应用 new 时,函数调用会变成“结构函数调用”。
2.3 技术
function Foo(name) {this.name = name;}
Foo.prototype.myName = function() {return this.name;};
var a = new Foo("a");
var b = new Foo("b");
a.myName(); // "a"
b.myName(); // "b"
这段代码展现了另外两种“面向类”的技巧:
this.name = name
给每个对象(也就是 a 和 b)都增加了 .name 属性,有点像类实例封装的数据值。Foo.prototype.myName = ...
可能个更乏味的技巧,它会给 Foo.prototype 对象增加一个属性(函数)。当初,a.myName() 能够失常工作,然而你可能会感觉很诧异,这是什么原理呢?
解释: 在创立的过程中,a 和 b 的外部 [[Prototype]] 都会
关联
到 Foo.prototype 上。当 a 和 b 中无奈找到 myName 时,它会(通过委托)在 Foo.prototype 上找到。
回顾“构造函数”
之前探讨 .constructor 属性时咱们说过,看起来 a.constructor === Foo 为真意味着 a 的确有一个指向 Foo 的 .constructor 属性,然而事实不是这样。
这是一个很可怜的误会。实际上,.constructor 援用同样被 委托
给了 Foo.prototype,而
Foo.prototype.constructor 默认指向 Foo。
Foo.prototype 的 .constructor 属性只是 Foo 函数在申明时的默认属性。
如果你创立了一个新对象并 替换
了函数默认的 .prototype 对象援用,那么 新对象并不会主动取得 .constructor 属性
。
function Foo() {...}
Foo.prototype = {...}; // 创立一个新原型对象
var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!
a1 并没有 .constructor 属性,所以它会委托 [[Prototype]] 链上的 Foo.prototype。
然而这个对象也没有 .constructor 属性(不过默认的 Foo.prototype 对象有这个属性!),所以它会持续委托,这次会委托给委托链顶端的 Object.prototype。
这个对象有 .constructor 属性,指向内置的 Object(..) 函数。
手动增加 .constructor 属性
你能够给 Foo.prototype 增加一个.constructor 属性,不过这须要手动增加一个合乎失常行为的不可枚举属性。
function Foo() {...}
Foo.prototype = {...}; // 创立一个新原型对象
// 须要在 Foo.prototype 上“修复”失落的 .constructor 属性
// 新对象属性起到 Foo.prototype 的作用
// 对于 defineProperty(..),Object.defineProperty( Foo.prototype, "constructor" ,
{
enumerable: false,
writable: true,
configurable: true,
value: Foo // 让 .constructor 指向 Foo
});
修复 .constructor 须要很多手动操作。
a1.constructor 是一个十分不牢靠并且不平安的援用。通常来说要尽量避免应用这些援用。
3.(原型)继承
咱们曾经看过了许多 JavaScript 程序中罕用的模仿类行为的办法,然而如果没有“继承”机制的话,JavaScript 中的类就只是一个空架子。
实际上,咱们曾经理解了通常被称作 原型继承的机制
,a 能够“继承”Foo.prototype
并拜访 Foo.prototype 的 myName()
函数。
如上图,它不仅展现出对象(实例)a1 到 Foo.prototype 的 委托
关系,还展现出
Bar.prototype 到 Foo.prototype 的委托关系,而后者和类继承很类似,只有箭头的方向不同。
图中由下到上的箭头表明这是 委托关联
, 不是复制操作
。
function Foo(name) {this.name = name;}
Foo.prototype.myName = function() {return this.name;};
function Bar(name,label) {Foo.call( this, name);
this.label = label;
}
// 咱们创立了一个新的 Bar.prototype 对象并关联到 Foo.prototype Bar.prototype = Object.create(Foo.prototype);
// 留神!当初没有 Bar.prototype.constructor 了
// 如果你须要这个属性的话可能须要手动修复一下它
Bar.prototype.myLabel = function() {return this.label;};
var a = new Bar("a", "obj a");
a.myName(); // "a"
a.myLabel(); // "obj a"
这段代码的外围局部就是语句
Bar.prototype = Object.create(Foo.prototype)。
调用 Object.create(..) 会凭空创立一个“新”对象并把新对象外部的 [[Prototype]] 关联到你指定的对象(本例中是 Foo.prototype)。
留神,上面这两种形式是常见的错误做法,实际上它们都存在一些问题:
- // 和你想要的机制不一样!
Bar.prototype = Foo.prototype; - // 基本上满足你的需要,然而可能会产生一些副作用 :(
Bar.prototype = new Foo();
Bar.prototype = Foo.prototype 并不会创立一个关联到 Bar.prototype 的新对象,它只是让 Bar.prototype 间接援用 Foo.prototype 对象
。
因而当你执行相似 Bar.prototype. myLabel = … 的赋值语句时会间接批改 Foo.prototype 对象自身。
Bar.prototype = new Foo() 确实会创立一个关联到 Bar.prototype 的新对象。然而它应用了 Foo(..) 的“结构函数调用”,如果函数 Foo 有一些副作用(比方写日志、批改状态、注册到其余对象、给 this 增加数据属性,等等)的话,就会影响到 Bar() 的“后辈”,结果不堪设想。
因而,要创立一个适合的关联对象,咱们必须应用
Object.create(..)
而不是应用具备副作用的 Foo(..)。这样做惟一的毛病: 就是须要创立一个新对象而后把旧对象摈弃掉,不能间接批改已有的默认对象。
一个规范并且牢靠的办法来批改对象的 [[Prototype]] 关联
- 在 ES6 之前,咱们只能通过设置
.__proto__
属性来实现,然而这个办法并不是规范并且无奈兼容所有浏览器。 - ES6 增加了辅助函数
Object.setPrototypeOf(..)
,能够用规范并且牢靠的办法来批改关联。
比照一下两种把 Bar.prototype 关联到 Foo.prototype 的办法
// ES6 之 前 需 要 抛 弃 默 认 的 Bar.prototype
Bar.ptototype = Object.create(Foo.prototype);
// ES6 开 始 可 以 直 接 修 改 现 有 的 Bar.prototype
Object.setPrototypeOf(Bar.prototype, Foo.prototype);
如果疏忽掉 Object.create(..) 办法带来的轻微性能损失(摈弃的对象须要进行垃圾回收),它实际上比 ES6 及其之后的办法更短而且可读性更高。
3.1 查看“类”关系
如何寻找对象 a 委托的对象(如果存在的话)呢?
在传统的面向类环境中, 查看一个实例(JavaScript 中的对象)的继承先人(JavaScript 中的委托关联)通常被称为 内省(或者反射)
。
function Foo() {// ...}
Foo.prototype.blah = ...;
var a = new Foo();
咱们如何通过内省找出 a 的“先人”(委托关联)呢?
- 第一种办法是站在“类”的角度来判断:
a instanceof Foo; // true
instanceof 操作符的左操作数是一个一般的对象,右操作数是一个函数。
instanceof 答复的问题是:在 a 的整条 [[Prototype]] 链中是否有指向 Foo.prototype 的对象?
这个办法只能解决: 对象(a)和函数(带 .prototype 援用的 Foo)之间的关系
。
如果想判断两个对象(比方 a 和 b)之间是否通过 [[Prototype]] 链关联,只用 instanceof 无奈实现。
- 第二种判断 [[Prototype]] 反射的办法,它更加简洁:
Foo.prototype.isPrototypeOf(a); // true
留神,在本例中,咱们实际上并不关怀(甚至不须要)Foo,咱们只须要一个能够用来判断的对象(本例中是 Foo.prototype)就行。
isPrototypeOf(..) 答复的问题是:在 a 的整条 [[Prototype]] 链中是否呈现过 Foo.prototype?
同样的问题,同样的答案,然而在第二种办法中并不需要间接援用函数(Foo),它的 .prototype 属性会被主动拜访
。
咱们只须要两个对象就能够判断它们之间的关系。
举例来说:
1// 非常简单:b 是否呈现在 c 的 [[Prototype]] 链中?2b.isPrototypeOf(c);
3.2 间接获取一个对象的 [[Prototype]] 链
-
在 ES5 中,规范的办法是:
Object.getPrototypeOf(a);
-
绝大多数(不是所有!)浏览器也反对一种非标准的办法来拜访外部 [[Prototype]] 属性:
a.__proto__
=== Foo.prototype; // true这个奇怪的
.__proto__
(在 ES6 之前并不是规范!)属性“神奇地”援用了外部的 [[Prototype]] 对象,如果你想间接查找(甚至能够通过.__proto__.__proto__
… 来遍历)原型链的话,这个办法十分有用。
4. 对象关联
[[Prototype]] 机制就是存在于对象中的一个外部链接,它会援用其余对象。
这个链接的作用是:如果在对象上没有找到须要的属性或者办法援用,引擎就会持续在 [[Prototype]] 关联的对象上进行查找。同理,如果在后者中也没有找到须要的援用就会持续查找它的 [[Prototype]],以此类推。这一系列对象的链接被称为“原型链”
。
4.1 创立关联
咱们曾经明确了为什么 JavaScript 的 [[Prototype]] 机制和类不一样(JS 中是关联而不是复制),也明确了它如何建设对象间的关联。
那 [[Prototype]] 机制的意义是什么呢?为什么 JavaScript 开发者费这么大的力量(模仿类)在代码中创立这些关联呢?
var foo = {something: function() {console.log( "Tell me something good...");
}
};
var bar = Object.create(foo);
bar.something(); // Tell me something good...
Object.create(..) 会创立一个新对象(bar)并把它关联到咱们指定的对象(foo),这样咱们就能够充分发挥 [[Prototype]] 机制的威力(委托)并且防止不必要的麻烦(比方应用 new 的结构函数调用会生成 .prototype 和 .constructor 援用)。
咱们并不需要类来创立两个对象之间的关系,只须要通过委托来关联对象就足够了。
而 Object.create(..) 不蕴含任何“类的诡计”,所以它能够完满地创立咱们想要的关联关系。
总结:
- 如果要拜访对象中并不存在的一个属性,[[Get]] 操作就会查找对象外部 [[Prototype]] 关联的对象。这个关联关系实际上定义了一条“原型链”(有点像嵌套的作用域链),在查找属性时会对它进行遍历。
- 所有一般对象都有内置的 Object.prototype,指向原型链的顶端(比如说全局作用域),如果在原型链中找不到指定的属性就会进行。
- toString()、valueOf() 和其余一些通用的性能都存在于 Object.prototype 对象上,因而语言中所有的对象都能够应用它们。
-
尽管 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很类似,然而 JavaScript 中的机制有一个外围区别,
那就是不会进行复制,对象之间是通过外部的[[Prototype]] 链关联的。
- 如何寻找对象 a 委托的对象(如果存在的话)呢?
-
第一种办法是站在“类”的角度来判断:
a instanceof Foo; // true
-
第二种判断 [[Prototype]] 反射的办法,它更加简洁:
Foo.prototype.isPrototypeOf(a); // true
b.isPrototypeOf(c);
- 间接获取一个对象的 [[Prototype]] 链
-
在 ES5 中,规范的办法是:
Object.getPrototypeOf(a);
-
绝大多数(不是所有!)浏览器也反对一种非标准的办法来拜访外部 [[Prototype]] 属性:
a.__proto__
=== Foo.prototype; // true
参考文章
- 《你所不晓得的 javaScript 上卷》