(上二)5-原型

[[Prototype]]

JavaScript 中的对象有一个特殊的 [[Prototype]] 内置属性,其实就是对于其他对象的引用。

我们知道,当你试图引用对象的属性时会触发默认的 [[Get]] 操作:

  • 首先,对象默认的内置 [[Get]] 操作首先在对象中查找是否有名称相同的属性;
  • 第二步,遍历可能存在的 [[Prototype]] 链,也就是原型链;
  • 无论如何都没有找到名称相同的属性,那 [[Get]] 操作会返回值 undefined

属性描述符(setter/getter)以及 ES6 中的 Proxy 会改变[[Get]]操作,暂且不谈。

我们这里着重看第二步,这个过程会持续到找到匹配的属性名或者查找完整条 [[Prototype]] 链。这里有一个比较特殊的例子 Object.create(..),它会创建一个对象并把这个对象的 [[Prototype]] 关联到指定的对象。

1
2
3
4
5
6
var anotherObject = {
a:2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );
myObject.a; // 2

使用 for..in 遍历对象时原理和查找 [[Prototype]] 链类似,任何可以通过原型链访问到的属性(并且是 enumerable=true)都会被枚举。使用 in 操作符来检查属性在对象中是否存在时,同样会查找对象的整条原型链(无论属性是否可枚举)

1
2
3
4
5
6
7
8
9
10
11
var anotherObject = {
a:2
};
// 创建一个关联到 anotherObject 的对象
var myObject = Object.create( anotherObject );

for (var k in myObject) {
console.log("found: " + k); // found: a
}

("a" in myObject); // true

Object.prototype

所有普通的 [[Prototype]] 链最终都会指向内置的 Object.prototype ,所以它包含 JavaScript 中许多通用的功能,如.toString().valueOf().hasOwnProperty(..).isPrototypeOf(..)

属性设置和屏蔽

给一个对象设置属性并不仅仅是添加一个新属性或者修改已有的属性值,在一些情况下变得复杂:

1
myObject.foo = "bar";
  • myObject 中包含 foo,只会修改已有的属性值
  • myObject 中包含 foo[[Prototype]] 中也包含 foo,那 么就会发生屏蔽。myObject 中包含的 foo 属性会屏蔽原型链上层的所有 foo 属性
  • myObject 中不包含 foo[[Prototype]] 中也不包含,foo 就会被直接添加到 myObject
  • myObject 中不包含 foo[[Prototype]] 中包含 foo 时:
    • 如果foo为普通数据访问属性并且没有被标记为只读(writable:false),那就会直接在 myObject 中添加一个名为 foo 的新属性,它是屏蔽属性。
    • 如果foo被标记为只读(writable:false),那么无法修改已有属性或者在 myObject 上创建屏蔽属性。如果运行在严格模式下,代码会 抛出一个错误。否则,这条赋值语句会被忽略。总之,不会发生屏蔽。
    • 如果在[[Prototype]]链上层存在foo并且它是一个setter,那就一定会 调用这个 setterfoo 不会被添加到myObject,也不会重新定义 foo 这个 setter

如果你希望在第二种和第三种情况下也屏蔽 foo,那就不能使用 = 操作符来赋值,而是使用 Object.defineProperty(..)来向 myObject 添加 foo

接下来我们了解一个为什么在 JS 中一个对象需要关联到另一个对象?

我们知道,JavaScript 和面向类的语言不同,它并没有类来作为对象的抽象模式
或者说蓝图。JavaScript 中只有对象,没有类。JavaScript 才是真正应该被称为“面向对象”的语言,因为它是少有的可以不通
过类,直接创建对象的语言。在 JavaScript 中,类无法描述对象的行为,(因为根本就不存在类!)对象直接定义自己的行为。

类函数

JavaScript 中没有类,我们一直在模仿类

JavaScript 与其他面向类的语言不通的是:

  • 在传统面向类的语言中,类可以被复制(或者说实例化)多次,就像用模具制作东西一样。之所以会这样是因为实例化(或者继承)一个类就意味着“把类的行为复制到物理对象中”,对于每一个新实例来说都会重复这个过程。
  • 在 JavaScript 中,并没有类似的复制机制。你不能创建一个类的多个实例,只能创建 多个对象,它们 [[Prototype]] 关联的是同一个对象。但是在默认情况下并不会进行复制, 因此这些对象之间并不会完全失去联系,它们是互相关联的。

构造函数

在 JS 中,我们常说的构造函数也只不过是一个普通的函数,但因为通过 new 来调用并构造了一个对象,我们称其为构造函数。但更准确地说法,函数不是构造函数,但是当且仅当使用 new 时,函数调用会变成“构造函数调用”。

技术

1
2
3
4
5
6
7
8
9
10
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"

JS 中的类在实例化过程中,ab 的内部 [[Prototype]] 都会关联到 Foo.prototype 上。当 ab 中无法找到 myName 时,它会通过委托在 Foo.prototype 上找到。

1
2
3
4
5
6
function Foo() { /* .. */ }
Foo.prototype = { /* .. */ }; // 创建一个新原型对象

var a1 = new Foo();
a1.constructor === Foo; // false!
a1.constructor === Object; // true!

Foo.prototype 的 .constructor 属性只是 Foo 函数在声明时的默认属性。如果你创建了一个新对象并替换了函数默认的 .prototype 对象引用,那么新对象并不会自动获得 .constructor 属性。

为什么会是Objecta1 并没有 .constructor 属性,所以它会委托 [[Prototype]] 链上的 Foo. prototype。但是这个对象也没有 .constructor 属性,所以它会继续委托,这次会委托给委托链顶端的 Object.prototype。这个对象有 .constructor 属性,指向内置的 Object(..) 函数。

(原型)继承

在 JS 类中实现继承,比较可靠的做法有两种:

  • ES6 之前 Bar.prototype = Object.create( Foo.prototype ); 这个会抛弃默认的 Bar.prototype
    • 会抛弃默认的 prototype
    • 如果需要,需手动修复prototype.constructor
  • ES6 开始 Object.setPrototypeOf( Bar.prototype, Foo.prototype ); 可以直接修改现有的 Bar.prototype
1
2
3
4
5
6
7
8
9
10
function Foo(name) {
this.name = name;
}
Foo.prototype.myName = function() {
return this.name;
};
function Bar(name,label) {
Foo.call( this, name );
this.label = label;
}

小结

1. 类、构造函数、原型、原型链?

2. JS 中的类与真正的面向类语言区别?

继承意味着复制操作,JavaScript(默认)并不会复制对象属性。相反,JavaScript 会在两个对象之间创建一个关联,这样一个对象就可以通过委托访问另一个对象的属性和函数。 委托这个术语可以更加准确地描述 JavaScript 中对象的关联机制。

0%