最近又重温了 js 的继承关系, 看了几篇帖子, 终于被我悟道了, 在此分享一下.
如果您没有耐心阅读完本文章, 请直接跳转至最后一段, 复制代码.
http://www.51testing.com/html/90/n-814890-2.html
推荐大家先看看这篇帖子, 等云里雾里的时候再看我下面的解释就豁然开朗了.
关键词: function, prototype, __proto__
javascript 对象是原型继承的, 当访问实例对象的某个属性而没有找到的时候, js 引擎会追溯这个对象的 prototype 链条, 尝试从超类, 超类的超类... 找到想要的属性; 如果失败返回 undefined, 成功则返回超类 prototype 对象的属性引用. 需要额外注意的是, 这个属性是只读的, "实例"是不可能改变"原型"中的属性的. 这段话相信大家都耳熟能详, 但它却是暗藏玄机, 需要细细的去解读一番.
1 prototype 只有类才具备, 实例类型是不具备的, 例如:
function First() {
}
var f = new First();
console.log(f.prototype);
会输出 undefined, 怎么样是不是同你的直觉相悖呢? 如果你打开 chrome 浏览器的调试功能, 你会发现 f 这个"实例"就没有 prototype 这个属性, 只有 __proto__ . 但这完全不矛盾, 定义只说:属性查找的时候会追溯原型链, 但幷没有说 prototype 是对象自身的. 它只属于对象的"类".
2 __proto__ 只属于实例, 或者说当一个对象可以作为"实例"时就具有了 __proto__ 例如
function First() {
}
启用调试模式就发现 First 同时具有 prototype 和 __proto__, 这是因为, 如果定义
var f = new First();// First 作为类型使用, 所以它具有 prototype, 而 First 本身是函数类型 Function 的一个实例, 所以也具有 __proto__ 属性.
3 对象的建立顺序是解释原型继承的关键. 援引帖子:
[quote]
先了解下new运算符,如下:
var a1 = new A;
var a2 = new A;
这是通过构造函数来创建对象的方式,那么创建对象为什么要这样创建而不是直接var a1 = {};呢?这就涉及new的具体步骤了,这里的new操作可以分成三步(以a1的创建为例):
1、新建一个对象并赋值给变量a1:var a1 = {};
2、把这个对象的[[Prototype]]属性指向函数A的原型对象:a1.[[Prototype]] = A.prototype
3、调用函数A,同时把this指向1中创建的对象a1,对对象进行初始化:A.apply(a1,arguments)[/quote]
按照上面所说, 对象建立是分为建立和初始化两个步骤的, 而初始化是通过 constructor 来完成的. 每个"类"类型就具有这样的结构
上面的三个步骤转化为伪码就是:
由于 js 的继承是实例继承, 所以 A.prototype 对象本身也是一个实例, 它自然具有 __proto__ 属性.
4 基于 1, 2, 3 我来解释下为什么继承的实现这么啰嗦:
整件事情就极其简单的被搞定了. 可遗憾的是你不能这么做, __proto__ 这个指针只是 FF 和 chrome 浏览器实现提的, 在规范中它叫做 [[Prototype]], 是不可以被改变的, 所以以上代码只是 hack 行为, 自己想想就行了.
再次基于以上所有知识, 让我们想法来绕过去
这时 b 对象的结构是:
b 的超类是 Object, 而我们需要的结构是:
有牛人(sorry 名字没记住)在 2006 就帮我们把这个死脑细胞的问题搞定了, 让我们一起来看看吧:
详细解释:
5 更多细节:
6 万事大吉. 让我们再来考虑如何把事情做的更优雅, 实际上在 ECMAScript 5th Edition 提供了函数 Object.create 能替代上面的 Extends. 基于这点, code 可以简化为:
Object.create 也支持额外的参数, 可以自行查阅相关资料.
以上所有痛苦是为了更优雅的解决问题, 而不单单只是为了解决问题. copy, paste 也要知其所以然.
如果您没有耐心阅读完本文章, 请直接跳转至最后一段, 复制代码.
http://www.51testing.com/html/90/n-814890-2.html
推荐大家先看看这篇帖子, 等云里雾里的时候再看我下面的解释就豁然开朗了.
关键词: function, prototype, __proto__
javascript 对象是原型继承的, 当访问实例对象的某个属性而没有找到的时候, js 引擎会追溯这个对象的 prototype 链条, 尝试从超类, 超类的超类... 找到想要的属性; 如果失败返回 undefined, 成功则返回超类 prototype 对象的属性引用. 需要额外注意的是, 这个属性是只读的, "实例"是不可能改变"原型"中的属性的. 这段话相信大家都耳熟能详, 但它却是暗藏玄机, 需要细细的去解读一番.
1 prototype 只有类才具备, 实例类型是不具备的, 例如:
function First() {
}
var f = new First();
console.log(f.prototype);
会输出 undefined, 怎么样是不是同你的直觉相悖呢? 如果你打开 chrome 浏览器的调试功能, 你会发现 f 这个"实例"就没有 prototype 这个属性, 只有 __proto__ . 但这完全不矛盾, 定义只说:属性查找的时候会追溯原型链, 但幷没有说 prototype 是对象自身的. 它只属于对象的"类".
2 __proto__ 只属于实例, 或者说当一个对象可以作为"实例"时就具有了 __proto__ 例如
function First() {
}
启用调试模式就发现 First 同时具有 prototype 和 __proto__, 这是因为, 如果定义
var f = new First();// First 作为类型使用, 所以它具有 prototype, 而 First 本身是函数类型 Function 的一个实例, 所以也具有 __proto__ 属性.
3 对象的建立顺序是解释原型继承的关键. 援引帖子:
[quote]
先了解下new运算符,如下:
var a1 = new A;
var a2 = new A;
这是通过构造函数来创建对象的方式,那么创建对象为什么要这样创建而不是直接var a1 = {};呢?这就涉及new的具体步骤了,这里的new操作可以分成三步(以a1的创建为例):
1、新建一个对象并赋值给变量a1:var a1 = {};
2、把这个对象的[[Prototype]]属性指向函数A的原型对象:a1.[[Prototype]] = A.prototype
3、调用函数A,同时把this指向1中创建的对象a1,对对象进行初始化:A.apply(a1,arguments)[/quote]
按照上面所说, 对象建立是分为建立和初始化两个步骤的, 而初始化是通过 constructor 来完成的. 每个"类"类型就具有这样的结构
// json 只是用来表示结构, 不是说"类"真的就是一个json
var someclass = {
prototype : {
constuctor : function used when init newly created object.
__proto__ : point to parent
}
}
上面的三个步骤转化为伪码就是:
function A() {
}
var a = {};// empty object [1]
a.__proto__ = A.prototype;// [2]
A.call(a, arguments);//[3]
由于 js 的继承是实例继承, 所以 A.prototype 对象本身也是一个实例, 它自然具有 __proto__ 属性.
4 基于 1, 2, 3 我来解释下为什么继承的实现这么啰嗦:
function A() {
this.name = 'Jack';
}
function B() {
}
// 如果我们想让 B 继承 A 的属性 name, 那么只要
var b = new B();
b.__proto__ = A.prototype;
整件事情就极其简单的被搞定了. 可遗憾的是你不能这么做, __proto__ 这个指针只是 FF 和 chrome 浏览器实现提的, 在规范中它叫做 [[Prototype]], 是不可以被改变的, 所以以上代码只是 hack 行为, 自己想想就行了.
再次基于以上所有知识, 让我们想法来绕过去
function B() {
}
var b = new B();
这时 b 对象的结构是:
b:{
__proto__: {
constructor: function B() {...}
__proto__: Object
}
}
b 的超类是 Object, 而我们需要的结构是:
b:{
__proto__: {
constructor: function B() {...}
__proto__: A
}
}
有牛人(sorry 名字没记住)在 2006 就帮我们把这个死脑细胞的问题搞定了, 让我们一起来看看吧:
function A() {
}
function B() {
}
function F() {
}
F.prototype = A.prototype;
var f = new F();
B.prototype = f;
B.prototype.constructor = B;
详细解释:
function A() {
}
function F() {
}
// 清晰起见, 我们设置
var APROTO = A.prototype
F.prototype = A.prototype;// F.prototype = APROTO;
var f = new F();// f.__proto__ = F.prototype = APROTO;
function B() {
}
B.prototype = f;
// js 默认会为每个类型创建一个 prototype 对象,
// 当然我们也可以指定 prototype 对象, 像这样 B.prototype = f,
// 则有, B.prototype = f = {__proto__ : APROTO} 这里尤其关键, 因为它折腾出了一个 __proto__ 指向 A 的对象 f.
var b = new B();
/*
根据实例建立的三个步骤 b.__proto__ = B.prototype = {__proto__ : APROTO}; b 的结构为:
b:{
__proto__: {
__proto__: APROTO
}
}
而之前的目标结构为:
b:{
__proto__: {
constructor: function B() {...}
__proto__: A
}
}
比较发现, 只要设置
B.prototyp.constructor = B;
目标和结果就完全一致了, 就实现了完美的继承.
*/
5 更多细节:
// 清晰起见我们定义tool函数
function Extends(superobj) {
function F() {
}
F.prototype = superobj;
return new F();
}
function A() {
}
// 如果要实现 B extends A, 则只要
function B() {}
B.prototype = Extends(A.prototype);
B.prototype.constructor = B;
var b = new B();
// successful? 这里忽略了一件事情, 就是 A 的私有属性没有在 B 中被初始化, 也就是没有 A.call(b, arguments);
// 让我们添加一点日志再次执行上面的代码
function A() {
console.log('A.constructor');
this.name = 'Jack';
}
function Extends(superobj) {
function F() {
console.log('F.constructor');
}
F.prototype = superobj;
return new F();
}
function B() {
console.log('B.constructor');
this.company = 'top secret';
}
B.prototype = Extends(A.prototype);
B.prototype.constructor = B;
var b = new B();
console.log(b.name);
console.log(b.company);
/*
输出:
F.constructor
B.constructor
undefined
top secret
*/
// 这时继承结构正确, 但变量初始化不正确, 所以需要修改 B
function B() {
A.call(this, arguments);// <-------
console.log('B.constructor');
this.company = 'top secret';
}
// 再次执行
var b = new B();
console.log(b.name);
console.log(b.company);
/*
输出:
F.constructor
A.constructor
B.constructor
Jack
top secret
*/
6 万事大吉. 让我们再来考虑如何把事情做的更优雅, 实际上在 ECMAScript 5th Edition 提供了函数 Object.create 能替代上面的 Extends. 基于这点, code 可以简化为:
function A() {};
function B() {A.call(this, arguments)};
B.prototype = Object.create(A.prototype);
B.prototype.constructor = B;
Object.create 也支持额外的参数, 可以自行查阅相关资料.
以上所有痛苦是为了更优雅的解决问题, 而不单单只是为了解决问题. copy, paste 也要知其所以然.