全面剖析javascript原型

前言

      javascript原型在javascript的继承机制上起到重要的作用,前几章我们也一直提到prototype,但没有说它到底是什么,这一章我们将围绕这个全面解析javascript中的继承魔法

正文

Prototype

      JavaScript 中的对象有一个特殊的 Prototype内置属性,其实就是对于其他对象的引用。几乎所有的对象在创建时 Prototype属性都会被赋予一个非空的值。当你试图引用对象的属性时会触发Get 操作,比如 myObject.a。对于默认的 Get操作来说,第一步是检查对象本身是否有这个属性,如果有的话就使用它。但是如果 a 不在 myObject 中,就需要使用对象的 Prototype链了

prototype的尽头

      原型链,从名字上看,是一条链子,那么这条链子的终点最终会指向哪里呢,但是到哪里是 [[Prototype]] 的“尽头”呢?所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype。由于所有的“普通”(内置,不是特定主机的扩展)对象都“源于”(或者说把 [[Prototype]] 链的顶端设置为)这个 Object.prototype 对象,所以它包含 JavaScript 中许多通用的功能。

属性设置

      给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值。比如看下面的例子:

myObject.foo = "bar";

      如果 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]] 链下层隐式创建(屏蔽)同名属性。这看起来有点奇怪,myObject 对象竟然会因为其他对象中有一个只读 foo 就不能包含 foo 属性。更奇怪的是,这个限制只存在于 = 赋值中,使用 Object.defineProperty(…) 并不会受到影响。

“类”

      看过我之前写的一些文章的同学可能知道,我已经反复提过,类在javascript只是起到一个语法糖的作用,javascript其实可以说是真正的“面向对象语言”,JavaScript 中只有对象,没有类!

"类"函数

      多年以来,JavaScript 中有一种奇怪的行为一直在被无耻地滥用,那就是模仿类。这种奇怪的“类似类”的行为利用了函数的一种特殊特性:所有的函数默认都会拥有一个名为 prototype 的公有并且不可枚举的属性,它会指向另一个对象。最直接的解释就是,这个对象是在调用 new Foo()时创建的,最后会被(有点武断地)关联到这个“Foo.prototype”对象上。

构造函数

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 属性,指向创建这个对象的函数”。

      实际上,Foo 和你程序中的其他函数没有任何区别。函数本身并不是构造函数,然而,当你在普通的函数调用前面加上 new 关键字之后,就会把这个函数调用变成一个“构造函数调用”。实际上,new 会劫持所有普通函数并用构造对象的形式来调用它。

js中模拟类的方式

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 给每个对象都添加了 .name 属性,有点像类实例封装的数据值。
  • Foo.prototype.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!

      对象的 .constructor 会默认指向一个函数,这个函数可以通过对象的
.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]] 关联到你指定的对象

      下面这两种方式是常见的错误做法,它们都存在一些问题:

// 和你想要的机制不一样!
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(…) 会创建一个新对象(bar)并把它关联到我们指定的对象(foo),这样我们就可以充分发挥 [[Prototype]] 机制的威力(委托)并且避免不必要的麻烦(比如使用 new 的构造函数调用会生成 .prototype 和 .constructor 引用)。

关联关系是备用

var anotherObject = {
	cool: function() {
		console.log( "cool!" );
	}
};
var myObject = Object.create( anotherObject );
myObject.cool(); // "cool!"

      由于存在 [[Prototype]] 机制,这段代码可以正常工作。但是如果你这样写只是为了让myObject 在无法处理属性或者方法时可以使用备用的 anotherObject,那么你的软件就会变得有点“神奇”,而且很难理解维护。

小结

      虽然这些 JavaScript 机制和传统面向类语言中的“类初始化”和“类继承”很相似,但是 JavaScript 中的机制有一个核心区别,那就是不会进行复制,对象之间是通过内部的[[Prototype]] 链关联的。相比之下,“委托”是一个更合适的术语,因为对象之间的关系不是复制而是委托。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值