ECMA-262-3深入解析第七章:2、OOP ECMAScript 实现

从来没有深入了解ECMA,网上找了一下,发现早在2010年就有大佬 Dmitry Soshnikov 总结了ECMA中的核心内容,我这里只是翻译记录,加深自己的印象。文章原文来自 ECMA-262-3 in detail. Chapter 7.2. OOP: ECMAScript implementation.

介绍

这是ECMAScript中关于面向对象编程(OOP)的第二部分。在第一部分中我们讨论了一般理论,并与ECMAScript进行了比较。在阅读这部分内容之前,如果有必要,我还是建议你先读一读OOP的第一部分,因为在本文中我们会用到第一部分的术语。你可以在这里找到:ECMA-262-3 详解:7.1、OOP一般理论

ECMAScript OOP 实现

通过一般理论的重点介绍以后,终于到了ECMAScript自身。现在,当我们知道其面向对象的方法时,我们再一次进行准确的定义:

ECMAScript是一种面向对象编程的语言,支持基于原型的委托继承。

我们才考虑数据类型开始分析。首先要注意到的就是ECMAScript区分原始值与对象上的实体。因此有时在各种文章中出现的“JavaScript中万物皆对象”的习语是不正确的(不完整的)。原始值涉及到某些类型的数据,我们详细来讨论一下。

数据类型

虽然ECMAScript是一个动态的弱类型语言,带有“鸭子”类型,以及自动类型转换,但它仍然具有某些数据类型。也就是说,某一时刻,一个对象属于一种具体类型。

标准定义了9个类型,但是在ECMAScript进程中只有6个是可以直接访问的:

  • Undefined
  • Null
  • Boolean
  • String
  • Number
  • Object

(译者注:当前2020年添加了Symbol以及BigInt类型)。

其他三个只有在实现级别可以访问(ECMAScript对象都不具有这种类型),并由规范用域解释某些操作行为,存储中间值等。它们是:

  • Reference
  • List
  • Completion

因此(简要概述), Reference 类型用于解释诸如 deletetypeofthis 等,并且由基础对象和属性名构成。 List 类型描述参数列表(在 new 表达式以及函数调用中)的表现。反过来, Completion 类型用域解释 breakcontinuereturnthrow 语句行为。

回到ECMAScript程序中使用的6个类型,前面的5个: UndefinedNullBooleanStringNumber 是原始值类型。(译者注: SymbolBigInt 也是原始值类型,也叫原始数据)。

原始值🌰:

1
2
3
4
5
var a = undefined;
var b = null;
var c = true;
var d = 'test';
var e = 10;

这些值直接在底层实现中表现。他们不是对象,他们没有原型,也没有构造器。

如果不能正确理解,typeof 操作符可能就不太直观了。典型的例子就是使用 typeof 操作符操作 null。当 typeof 操作符操作 null 时,无论是否将 null 指定给 Null,结果都是 "object"

1
console.log(typeof null); // "object"

原因是因为 typeof 操作符返回从标准表中获取的值,简单地说:“对于 null 值应该返回 "object" 字符串”。

规范并没有澄清此事,但是 Brendan Eich(Javascript创造者)注意到, nullundefined 相比, null 主要用于出现对象的地方,即与对象密切相关的本质(意味着对象的“空”引用,很可能是为未来目的保留了一个位置)。但是,在一些草稿中,这种“现象”被当作一个平常bug描述的地方提供了文档。而且,这个bug出现的Brendan Eich也参与的一个bug跟踪程序中。结果,尽管ECMA-262-3标准将 null 类型定义为 Null ,但是还是决定保留 typeof null 不变,即 "object"

对象类型

反过来, Object 类型(不要被 Object 构造函数混淆,我们只讨论抽象的类型)是代表ECMAScirpt对象的唯一类型。

对象是一个键值对的无序集合。

对象的键成为属性。属性是原始值与其他对象的容器。如果属性包含函数作为值,那它们被称为方法。

🌰:

1
2
3
4
5
6
7
8
9
10
11
12
var x = { // object "x" with three properties: a, b, c
a: 10, // primitive value
b: {z: 100}, // object "b" with property z
c: function () { // function (method)
console.log('method x.c');
}
};

console.log(x.a); // 10
console.log(x.b); // [object Object]
console.log(x.b.z); // 100
x.c(); // 'method x.c'

动态性质

正如我们在7.1章节中提到的,ES中的对象是完全动态的。这就意味着我们可以在程序执行的任何时候对对象的属性进行添加,修改以及移除。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = {x: 10};

// add new property
foo.y = 20;
console.log(foo); // {x: 10, y: 20}

// change property value to function
foo.x = function () {
console.log('foo.x');
};

foo.x(); // 'foo.x'

// delete property
delete foo.x;
console.log(foo); // {y: 20}

一些属性不能被修改,不可配置的属性,即只读属性或者是已删除的属性。我们将在下面的章节考虑这些。

Note:ES5标准定化了静态对象,不能用新属性扩展,任何属性也不能被修改或者删除。这些成为冻结对象,可以通过 Object.freeze(o) 方法获得。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var foo = {x: 10};

// freeze the object
Object.freeze(foo);
console.log(Object.isFrozen(foo)); // true

// can't modify
foo.x = 100;

// can't extend
foo.y = 200;

// can't delete
delete foo.x;

console.log(foo); // {x: 10}

而且,使用 Object.preventExtensions(o) 方法只阻止扩展也是可以的,或者,使用 Object,defineProperty(o) 方法控制特殊的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var foo = {x : 10};

Object.defineProperty(foo, "y", {
value: 20,
writable: false, // read-only
configurable: false // non-configurable
});

// can't modify
foo.y = 200;

// can't delete
delete foo.y; // false

// prevent extensions
Object.preventExtensions(foo);
console.log(Object.isExtensible(foo)); // false

// can't add new properties
foo.z = 30;

console.log(foo); {x: 10, y: 20}

想要了解详情,阅读这篇文章

内置,原生与宿主对象

有必要注意的是,规范区分了本地对象,内置对象以及宿主对象。

内置对象和原生对象是由ECMAScript规范和实现定义,他们之前的区别不明显。 原生对象是ECMAScript实现提供的所有对象(其中一些可以内置,一些可以在程序执行期间创建,例如用户定义的对象)。

内置对象可以算是原生对象的一个子类,在程序开始之前已经内置到了ECMAScript中(例如 parseIntMath 等)。

所有的宿主对象都是由宿主环境提供,代表是浏览器,包含了许多宿主对象,例如 windowconsole.log 等等。

注意,宿主对象可以使用ES自身实现,并且完全遵循规范语义。从这点来看,他们可以被叫做“原生宿主”对象(非正式的叫法),虽然主要是理论上的。规范并没有定义任何“原生宿主”的概念。

Boolean, String和Number对象

对于一些原始事物,规范定义了特殊的包装对象。他们是以下对象:

  • Boolean 对象
  • String 对象
  • Number 对象

这些对象是由相应的内置构造函数创建的,并包含了内置属性作为原始值。对象表示的可以转行为原始值,反之亦然。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var c = new Boolean(true);
var d = new String('test');
var e = new Number(10);
console.log(typeof c); // object
console.log(typeof d); // object
console.log(typeof e); // object

// converting to primitive
// conversion: ToPrimitive
// applying as a function, without "new" keyword
с = Boolean(c);
d = String(d);
e = Number(e);
console.log(typeof c); // boolean
console.log(typeof d); // string
console.log(typeof e); // number

// back to Object
// conversion: ToObject
с = Object(c);
d = Object(d);
e = Object(e);
console.log(typeof c); // object
console.log(typeof d); // object
console.log(typeof e); // object

除此之外,也有通过特殊内置对象创建的对象: Function (函数对象构造器), Array (数组构造器), RegExp (正则表达式构造器), Math (数学方法), Date (日期构造器)等等。这些对象也是 Object 类型的值,他们之前的区别是由内部属性管理的,我们将在下面讨论。

文字符号(字面符号)

对于三个对象值:object,array和regular表达式,有短记号,他们分别成为对象初始化程序,数组初始化程序与正则表达式文字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// equivalent to new Array(1, 2, 3);
// or array = new Array();
// array[0] = 1;
// array[1] = 2;
// array[2] = 3;
var array = [1, 2, 3];

// equivalent to
// var object = new Object();
// object.a = 1;
// object.b = 2;
// object.c = 3;
var object = {a: 1, b: 2, c: 3};

// equivalent to new RegExp("^\\d+$", "g")
var re = /^\d+$/g;

注意,在重新分配名称(ObjectArray,或者RegExp)到新对象的情况下,后续使用文字符号的语义在实现中可能会有所不同。例如在当前(2010.3.4)Rhino实现中或者是在旧的SpiderMonkey 1.7版本中,适当的文字符号将创建一个与构造函数名称的值相对应的对象。在其他的实现中,即使构造函数名称重新绑定到新对象,也不会更改文字符号的语义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
var getClass = Object.prototype.toString;

Object = Number;

var foo = new Object;
console.log([foo, getClass.call(foo)]); // 0, "[object Number]"

var bar = {};

// in Rhino, SpiderMonkey 1.7 - 0, "[object Number]"
// in other: still "[object Object]", "[object Object]"
console.log([bar, getClass.call(bar)]);

// the same with Array name
Array = Number;

foo = new Array;
console.log([foo, getClass.call(foo)]); // 0, "[object Number]"

bar = [];

// in Rhino, SpiderMonkey 1.7 - 0, "[object Number]"
// in other: still "", "[object Object]"
console.log([bar, getClass.call(bar)]);

// but for RegExp, semantics of the literal
// isn't being changed in all tested implementations

RegExp = Number;

foo = new RegExp;
console.log([foo, getClass.call(foo)]); // 0, "[object Number]"

bar = /(?!)/g;
console.log([bar, getClass.call(bar)]); // /(?!)/g, "[object RegExp]"

正则表达式字面量以及RegExp对象

注意,在ES3中,正则表达式在语义上等效的后两种情况仍然有所不同。正则字面量只存在一个实例中,并在解析阶段被创建,而 RegExp 构造函数总是会创建一个新的对象。这可能会造成一些问题,例如当regexp测试失败的时候,regexp对象的lastIndex属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
for (var k = 0; k < 4; k++) {
var re = /ecma/g;
console.log(re.lastIndex); // 0, 4, 0, 4
console.log(re.test("ecmascript")); // true, false, true, false
}

// in contrast with

for (var k = 0; k < 4; k++) {
var re = new RegExp("ecma", "g");
console.log(re.lastIndex); // 0, 0, 0, 0
console.log(re.test("ecmascript")); // true, true, true, true
}

注意:在ES5中这个问题被修复了,正则字面量也是创建一个新的对象了。

关联数组?

通常在各种文章或者讨论中,Javascript对象(通常是通过声明形式创建的对象,通过对象初始化 {})被称为哈希表或者更简单的哈希(Ruby或者Perl中的术语),关联数组(PHP中的术语),词典(Python中的术语)。

使用这种术语是具体技术的习惯。真的,他们足够相似,并且对于“键-值”对,存储完全对应于理论上的“关联数组”或“哈希表”数据结构。而且,哈希表抽象数据类型可能并且通常是用在实现级别。

然而,虽然用术语了的描述概念上的思维方式,但是对于ECMAScript而言,从技术上而言,这是不正确的。正如之前提到的,ECMAScript仅仅只有一个对象类型,并且关于“键-值”对存储的“子类型”互不影响。因此,没有单独的特殊术语(哈希或者其他)。因为任何对象(无论它的内部属性如何)都可以存储以下对:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var a = {x: 10};
a['y'] = 20;
a.z = 30;

var b = new Number(1);
b.x = 10;
b.y = 20;
b['z'] = 30;

var c = new Function('');
c.x = 10;
c.y = 20;
c['z'] = 30;

// etc. – with any object "subtype"

而且,因为委派,ECMAScript中的对象可以是非空的,因此术语“哈希”也可以是不正确的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Object.prototype.x = 10;

var a = {}; // create "empty" "hash"

console.log(a["x"]); // 10, but it's not empty
console.log(a.toString); // function

a["y"] = 20; // add new pair to "hash"
console.log(a["y"]); // 20

Object.prototype.y = 20; // and property into the prototype

delete a["y"]; // remove
console.log(a["y"]); // but key and value are still here – 20

注意,ES5标准了无须原型即可创建对象的功能,即他们的原型被设置为 null 。使用 Object.create(null) 方法实现。从这点出发,这种对象是简单的哈希表:

1
2
var aHashTable = Object.create(null);
console.log(aHashTable.toString); // undefined

而且,一些属性可能有特殊的 getters/setters,因此也可能造成混淆:

1
2
3
var a = new String("foo");
a['length'] = 10;
console.log(a['length']); // 3

但是,即使考虑到“哈希”可能具有“原型”(例如,在Ruby或者Python中,代表哈希对象的类)。在ECMAScript中这个术语也可能不正确,因为在各种属性访问器之间没有语义上的区别(例如点和括号符号)。

同样在ECMAScript中,“属性”的概念在语义上没有分为“键”, “数组索引”, “方法” 或者“属性”。在这里,他们所有都是用过检查原型链来遵守读/写算法的一般规则的属性。

下面的Ruby的例子中,我们看到语义上的区别,因此,这里这些术语可能不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
a = {}
a.class # Hash

a.length # 0

# new "key-value" pair
a['length'] = 10;

# but semantics for the dot notation
# remains other and means access
# to the "property/method", but not to the "key"

a.length # 1

# and the bracket notation
# provides access to "keys" of a hash

a['length'] # 10

# we can augment dynamically Hash class
# with new properties/methods and they via
# delegation will be available for already created objects

class Hash
def z
100
end
end

# a new "property" is available

a.z # 100

# but not a "key"

a['z'] # nil

ECMA-262-3标准没有定义“哈希”(以及类似的)的概念。但是,如果要使用理论上的数据结构,也可以这样命名。

类型转换

将一个对象转换为一个原始值,可以使用 valueOf 方法。正如我们提到的,构造函数(对于某些类型)作为函数调用,例如,没有 new 操作符的情况下,将对象类型转换为一个原始值。对于这种转换,隐式调用了 valueOf 方法。

1
2
3
4
5
6
7
8
9
var a = new Number(1);
var primitiveA = Number(a); // implicit "valueOf" call
var alsoPrimitiveA = a.valueOf(); // explicit

console.log([
typeof a, // "object"
typeof primitiveA, // "number"
typeof alsoPrimitiveA // "number"
]);

这个方法运行对象参与各种操作,例如,相加:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var a = new Number(1);
var b = new Number(2);

console.log(a + b); // 3

// or even so

var c = {
x: 10,
y: 20,
valueOf: function () {
return this.x + this.y;
}
};

var d = {
x: 30,
y: 40,
// the same .valueOf
// functionality as "с" object has,
// borrow it:
valueOf: c.valueOf
};

console.log(c + d); // 100

默认情况下, valueOf 方法的值(如果没有被覆盖)可以根据对象类型有所不同。对于一些对象返回的是 this 值,例如 Object.prototype.valueOf() ,对于其他的返回任何可计算的值。例如 Date.prototype.valueOf() ,返回的是date的时间:

1
2
3
4
5
6
var a = {};
console.log(a.valueOf() === a); // true, "valueOf" returned this value

var d = new Date();
console.log(d.valueOf()); // time
console.log(d.valueOf() === d.getTime()); // true

另外,还有一个对象的原始表示形式 — 字符串表示形式。这个由 toStirng 方法负责,在一些操作中也是自动执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var a = {
valueOf: function () {
return 100;
},
toString: function () {
return '__test';
}
};

// in this operation
// toString method is
// called automatically
console.log(a); // "__test" 注意:在最新的 chrome 中 并不是作者说的这个值 而是 对象 a

// but here - the .valueOf() method
console.log(a + 10); // 110

// but if there is no
// valueOf method, it
// will be replaced with the
//toString method
delete a.valueOf;
console.log(a + 10); // "_test10"

toString 方法定义在 Object.prototype 上,有着特殊的意义。他返回了内置的 [[Class]] 属性的值,我们将在下面讨论这个值。

除了 ToPrimitive 转换外,还有 ToObject 转换,反之亦然,将值转换为对象类型。

一个明确的调用 ToObject 的方法就是将内置的 Object 构造函数作为普通函数调用(对于某些类型,使用带有 new 操作符的 Object 也是可能的):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var n = Object(1); // [object Number]
var s = Object('test'); // [object String]

// also for some types it is
// possible to call Object with new operator
var b = new Object(true); // [object Boolean]

// but applied without arguments,
// new Object creates a simple object
var o = new Object(); // [object Object]

// in case if argument for Object function
// is already object value,
// it simply returns
var a = [];
console.log(a === new Object(a)); // true
console.log(a === Object(a)); // true

就带有 new 与不带有 new 操作符调用内置构造函数而言,没有通用规则,这取决于构造函数。例如 ArrayFunction 构造函数,在作为一个构造函数调用(有 new )和作为普通函数(没有 new )调用提供相同的结果。

1
2
3
4
5
6
var a = Array(1, 2, 3); // [object Array]
var b = new Array(1, 2, 3); // [object Array]
var c = [1, 2, 3]; // [object Array]

var d = Function(''); // [object Function]
var e = new Function(''); // [object Function]

运用某些操作的时候,还存在显示与隐式的转换:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
var b = 2;

// implicit
var c = a + b; // 3, number
var d = a + b + '5' // "35", string

// explicit
var e = '10'; // "10", string
var f = +e; // 10, number
var g = parseInt(e, 10); // 10, number

// etc.

属性的属性

属性的属性

所有的属性都有大量的属性:

  • {readOnly} :尝试重写属性值是被忽略的;但是ReadOnly属性可以被宿主环境操作修改,因此,ReadOnly不意味着“不变的值”。
  • {DontEnum} :属性不能通过 for...in 循环枚举
  • {DontDelete}delete 操作用于该属性将被忽视
  • {Internal} :属性是内部的,没有名字并且只能用域实现级别;这类属性不能在ECMAScript程序中访问。

注意,在ES5中{ReadOnly}, {DontEnum} and {DontDelete} 被重命名为相应的 [[Writable]] , [[Enumerable]][[Configurable]] 并且可以通过 Object.defineProperty 和相似的方法手动管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var foo = {};

Object.defineProperty(foo, "x", {
value: 10,
writable: true, // aka {ReadOnly} = false
enumerable: false, // aka {DontEnum} = true
configurable: true // {DontDelete} = false
});

console.log(foo.x); // 10

// attributes set is called a descriptor
var desc = Object.getOwnPropertyDescriptor(foo, "x");

console.log(desc.enumerable); // false
console.log(desc.writable); // true
// etc.

内部属性与方法

对象也可以拥有大量的内部属性,这些属性是实现的一部分,不能直接供ECMAScript程序访问(但是正如我们下面将看到的,某些实现是允许访问这些属性的)。按照约定,这些属性用双方括号 [[]] 括起来。

我们将会接触到一些(所有对象都是必须的);其他属性的描述可以在规范中找到。

每个对象都应该实现以下的内部属性与方法:

  • [[Prototype]] :对象的原型(将会在下面详细说明)。
  • [[Class]] :一个代表对象类型的字符串(例如: ObjectArrayFunction 等等);用于区分对象。
  • [[Get]] :用于获取属性值的方法
  • [[Put]] :用于设置属性值的方法
  • [[CanPut]] : 检测是否可以重写属性
  • [[HasProperty]] :检测对象是否已经拥有这个属性
  • [[Delete]] :从对象中移除属性
  • [[DefaultValue]] :返回与对象相应的原始值(对于获取值, valueOf 方法被调用,对于一些对象,抛出 TypeError 异常)。

要从ECMAScript程序中获得 [[Class]] 属性,通过 Object.property.toString() 方法可以间接获得。这个方法可以返回如下字符串 "[Object " + [[Class]] + "]" 。例如:

1
2
3
4
5
6
var getClass = Object.prototype.toString;

getClass.call({}); // [object Object]
getClass.call([]); // [object Array]
getClass.call(new Number(1)); // [object Number]
// etc.

这个特征常用于检测对象的类型,然而,有必要注意的是,通过规范,宿主对象的内部 [[Class]] 属性可以是任何值,包括内置对象的 [[Class]] 值,从理论上将,不会进行100%证明的这种检查。例如,在老版本的IE中, document.childNodes.item(...) 方法的 [[Class]] 属性返回 "String" (在其他的实现中,返回 "Function"

1
2
// in older IE - "String", in other - "Function"
console.log(getClass.call(document.childNodes.item));

构造函数

我们上面提及到,ECMAScript中对象是通过叫做构造函数的东西创建的。

构造函数是一个创建与初始化新创建对象的函数。

对于创建(内存分配),由构造函数内部的 [[Constuct]] 方法负责。指定了这个内部方法的行为,并且所有构造函数都使用这种方法为新对象分配内存。

通过在新创建的对象的上下文中调用函数来管理初始化。构造函数中已经存在的内部 [[Call]] 方法负责此行为。

注意,用户代码中只能访问初始阶段。因此,即使从初始化开始,我们可以忽略第一步中创建的 this 对象返回不同的对象:

1
2
3
4
5
6
7
8
9
function A() {
// update newly created object
this.x = 10;
// but return different object
return [1, 2, 3];
}

var a = new A();
console.log(a.x, a); undefined, [1, 2, 3]

定位到第五章函数中讲到的函数对象的创建法则,我们看到函数是一个本地对象,除其他属性外,还有内部的 [[Construct]][[Call]] 属性以及显示的 原型 属性 — 对未来对象原型的引用(注意,这里以及下面的 NativeObject 是我对ECMA-262-3中”本地对象”的伪代码命名约定,但不是内置构造函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
F = new NativeObject();

F.[[Class]] = "Function"

.... // other properties

F.[[Call]] = <reference to function> // function itself

F.[[Construct]] = internalConstructor // general internal constructor

.... // other properties

// prototype of objects created by the F constructor
__objectPrototype = {};
__objectPrototype.constructor = F // {DontEnum}
F.prototype = __objectPrototype

因此,除了 [[Class]] 属性(等同于 "Function")之外的 [[Call]] 是区分对象的主要方法。因此,具有内部 [[Call]] 属性的对象被称为 functions。对于这些对象, typeof 操作符返回 "function" 值。因此,它主要与本地对象有关,在主机可以调用对象的情况下,某些实现中的 typeof 操作符(不少于 [[Class]] 属性)可能返回其他的值,例如,在IE中的 window.console.log(...)

1
2
3
// in IE - "Object", "object", in other - "Function", "function"
console.log(Object.prototype.toString.call(window.console.log));
console.log(typeof window.console.log); // "Object"

内部的 [[Construct]] 方法由用于构造函数的 new 操作符激活。正如我们说的,这个方法负责内存分配以及新对象的创建。如果没有参数,则可以省略构造函数的括号:

1
2
3
4
5
6
7
8
9
10
11
12
13
function A(x) { // constructor А
this.x = x || 10;
}

// without arguments, call
// brackets can be omitted
var a = new A; // or new A();
console.log(a.x); // 10

// explicit passing of
// x argument value
var b = new A(20);
console.log(b.x); // 20

并且,构造函数(初始阶段)中的this值被设置为新创建的这个对象。

我们来细想一下对象创建的计算规则。

对象创建的计算规则

内部的 [[Constuct]] 方法的行为可以被表示为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
F.[[Construct]](initialParameters):

O = new NativeObject();

// property [[Class]] is set to "Object", i.e. simple object
O.[[Class]] = "Object"

// get the object on which
// at the moment references F.prototype
var __objectPrototype = F.prototype;

// if __objectPrototype is an object, then:
O.[[Prototype]] = __objectPrototype
// else:
O.[[Prototype]] = Object.prototype;
// where O.[[Prototype]] is the prototype of the object

// initialization of the newly created object
// applying the F.[[Call]]; pass:
// as this value – newly created object - O,
// arguments are the same as initialParameters for F
R = F.[[Call]](initialParameters); this === O;
// where R is the returned value of the [[Call]]
// in JS view it looks like:
// R = F.apply(O, initialParameters);

// if R is an object
return R
// else
return O

注意两个主要的特征:

第一,创建对象的 prototype 属性是来自于当前时刻函数的 prototype 属性(这就意味着从一个构造函数创建的两个对象的属性可以改变,因为函数的 prototype 属性也可以改变)。

第二,我们上面提到,如果在对象初始时 [[Call]] 返回了一个对象,则将其作为整个 new 表达式的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function A() {}
A.prototype.x = 10;

var a = new A();
console.log(a.x); // 10 – by delegation, from the prototype

// set .prototype property of the
// function to new object; why explicitly
// to define the .constructor property,
// will be described below
A.prototype = {
constructor: A,
y: 100
};

var b = new A();
// object "b" has new prototype
console.log(b.x); // undefined
console.log(b.y); // 100 – by delegation, from the prototype

// however, prototype of the "a" object
// is still old (why - we will see below)
console.log(a.x); // 10 - by delegation, from the prototype

function B() {
this.x = 10;
return new Array();
}

// if "B" constructor had not return
// (or was return this), then this-object
// would be used, but in this case – an array
var b = new B();
console.log(b.x); // undefined
console.log(Object.prototype.toString.call(b)); // [object Array]

我们再来深入的了解一下原型。

Prototype

每一个对象都含有一个原型(某些系统对象可以例外)。通过内部的,隐式的和不可访问的 [[Protorype]] 属性来组织与原型的通信。一个属性可以是一个对象也可以是一个 null 对象。

Property constructor

在上面的例子中有两点很重要。第一与函数prototype属性的构造函数属性有关。

正如我们在函数对象创建算法中看到的,在函数创建阶段, constructor 属性设置到函数的 prototype 属性中。此属性的值是对函数本身的循环引用

1
2
3
4
function A() {}
var a = new A();
console.log(a.constructor); // function A() {}, by delegation
console.log(a.constructor === A); // true

通常这种情况下有误解constructor 属性错误的被当作创建对象的自身属性。然而,正如我们所看到的,这个属性属于原型,并且通过继承访问对象。

通过继承的 constructor 属性实例可以间接的获取原型对象的引用:

1
2
3
4
5
6
7
8
9
10
11
function A() {}
A.prototype.x = new Number(10);

var a = new A();
console.log(a.constructor.prototype); // [object Object]

console.log(a.x); // 10, via delegation
// the same as a.[[Prototype]].x
console.log(a.constructor.prototype.x); // 10

console.log(a.constructor.prototype.x === a.x); // true

注意,函数的 constructorprototype 可以在对象创建后重新定义。这种情况下,对象失去通过上面机制的引用。

如果我们在原来的原型中通过函数的prototype 属性添加新的或者修改已经存在的属性,实例将会看到最新的添加的属性。

但是,如果我们完全修改了函数的prototype 属性(通过赋值一个新对象),失去原来构造函数(以及原来的原型)的引用。这是因为新创建的对象没有constructor 属性。

1
2
3
4
5
6
7
8
function A() {}
A.prototype = {
x: 10
};

var a = new A();
console.log(a.x); // 10
console.log(a.constructor === A); // false!

因此,引用应该手动修复:

1
2
3
4
5
6
7
8
9
function A() {}
A.prototype = {
constructor: A,
x: 10
};

var a = new A();
console.log(a.x); // 10
console.log(a.constructor === A); // true

注意,手动添加的 constructor 属性与原来的相比,没有 {DontEnum} 属性,结果就是,在 A.prototype 中可以通过 for...in 循环枚举:

ES5中引入了通过[[Enumerable]]属性控制属性的可枚举状态的能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var foo = {x: 10};

Object.defineProperty(foo, "y", {
value: 20,
enumerable: false // aka {DontEnum} = true
});

console.log(foo.x, foo.y); // 10, 20

for (var k in foo) {
console.log(k); // only "x"
}

var xDesc = Object.getOwnPropertyDescriptor(foo, "x");
var yDesc = Object.getOwnPropertyDescriptor(foo, "y");

console.log(
xDesc.enumerable, // true
yDesc.enumerable // false
);

显示 prototype 与隐式的 [[Prototype]] 属性

通过函数的prototype属性直接引用原型,通常对象的原型被错误的混淆。是的,真的,他引用了相同的对象,即对象的 [[Prototype]] 属性:

1
a.[[Prototype]] ----> Prototype <---- A.prototype

而且,在对象创建阶段,实例的 [[Prototype]] 完全从构造函数的prototype属性上获取值。

然而,替换构造函数的prototype属性不影响已经创建的对象的原型。仅仅只是它的构造函数的prototype属性改变了。这意味着新对象会有一个新的原型。但是已经创建的对象(在 prototype 属性修改之前),拥有旧的原型的引用,并且引用不能被修改。

1
2
3
4
5
6
// was before changing of A.prototype
a.[[Prototype]] ----> Prototype <---- A.prototype

// became after
A.prototype ----> New prototype // new objects will have this prototype
a.[[Prototype]] ----> Prototype // reference to old prototype

🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function A() {}
A.prototype.x = 10;

var a = new A();
console.log(a.x); // 10

A.prototype = {
constructor: A,
x: 20
y: 30
};

// object "а" delegates to
// the old prototype via
// implicit [[Prototype]] reference
console.log(a.x); // 10
console.log(a.y) // undefined

var b = new A();

// but new objects at creation
// get reference to new prototype
console.log(b.x); // 20
console.log(b.y) // 30

因此,有时候在关于JavaScript的文章中会出现声明,声称“动态修改原型将会影响所有的对象,并且他们将会拥有新的原型是错误的。新的原型只有在这个修改以后创建的新对象才有。

这里主要的规则是:对象的原型是在对象创建那一刻设置的,并且在那之后不能被修改为新对象。如果构造函数仍引用相同的对象,则使用该对象的显示原型引用,只能添加新的属性或者修改对象原型已经存在的属性。

非标准的 __proto__ 属性

然而,一些实现中,例如,SpiderMonkey,通过非标准的 __proto__ 属性提供对象原型的显示引用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function A() {}
A.prototype.x = 10;

var a = new A();
console.log(a.x); // 10

var __newPrototype = {
constructor: A,
x: 20,
y: 30
};

// reference to new object
A.prototype = __newPrototype;

var b = new A();
console.log(b.x); // 20
console.log(b.y); // 30

// "a" object still delegates
// to the old prototype
console.log(a.x); // 10
console.log(a.y); // undefined

// change prototype explicitly
a.__proto__ = __newPrototype;

// now "а" object references
// to new object also
console.log(a.x); // 20
console.log(a.y); // 30

注意,ES5中引进了 Object.getPrototypeOf(o) 方法,直接返回对象的 [[Prototype]] 属性 — 实例原来的属性。但是,与 __proto__ 相比,作为一个 getter ,他不被允许设置属性。

1
2
var foo = {};
Object.getPrototypeOf(foo) == Object.prototype; // true

对象独立于其构造函数

因为实例的原型独立于构造函数和构造函数的prototype属性,构造函数在完成它的主要目的(创建对象)后,可以移除。原型对象将会继续存在,通过 [[Prototype]] 属性被引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
function A() {}
A.prototype.x = 10;

var a = new A();
console.log(a.x); // 10

// set "А" to null - explicit
// reference on constructor
A = null;

// but, still possible to create
// objects via indirect reference
// from other object if
// .constructor property has not been changed
var b = new a.constructor();
console.log(b.x); // 10

// remove implicit reference
// after it `a.constructor`, and `b.constructor`
// will point to the default Object function, but not `A`
delete a.constructor.prototype.constructor;

// it is not possible to create objects
// of "А" constructor anymore, but still
// there are two such objects which
// still have reference to their prototype
console.log(a.x); // 10
console.log(b.x); // 10

instanceof 操作的特征

通过构造函数的prototype属性显示引用原型,与 instanceof 操作符的工作相关。

该操作符与对象的原型链完全协同工作,但不与构造函数本身协同工作。考虑到这一点,因为这里经常有误解。因此,有一个检查:

1
2
3
if (foo instanceof Foo) {
...
}

这并不意味着检查 foo 对象是否由 Foo 构造函数创建。

instanceof 操作符的所有工作只是获取 Foo.prototype 属性的值以及从 foo.[[Prototype]] 开始检查 foo 的作用域链是否存在。 instanceof 操作符是由构造函数内部的 [[HasInstace]] 方法激活。

从例子出发来看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function A() {}
A.prototype.x = 10;

var a = new A();
console.log(a.x); // 10

console.log(a instanceof A); // true

// if set A.prototype
// to null...
A.prototype = null;

// ...then "a" object still
// has access to its
// prototype - via a.[[Prototype]]
console.log(a.x); // 10

// however, instanceof operator
// can't work anymore, because
// starts its examination from the
//prototype property of the constructor
console.log(a instanceof A); // error, A.prototype is not an object

另一方面,可以由一个构造函数创建对象,但是通过另一个对象检测 instanceof 将会返回 true 。很有必要设置对象的 [[Prototype]] 属性和构造函数的 prototype 属性到相同的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function B() {}
var b = new B();

console.log(b instanceof B); // true

function C() {}

var __proto = {
constructor: C
};

C.prototype = __proto;
b.__proto__ = __proto;

console.log(b instanceof C); // true
console.log(b instanceof B); // false

原型作为方法和共享属性的存储

在ECMAScript中原型 最有用的应用是存储对象的方法、默认状态和共享属性。

的确,对象可以拥有自己的状态,当时方法通常都是相同的。因此,为了优化内存使用,方法通常被定义在原型上面。这意味着所有通过构造函数创建的实例总是共享相同的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function A(x) {
this.x = x || 100;
}

A.prototype = (function () {

// initializing context,
// use additional object

var _someSharedVar = 500;

function _someHelper() {
console.log('internal helper: ' + _someSharedVar);
}

function method1() {
console.log('method1: ' + this.x);
}

function method2() {
console.log('method2: ' + this.x);
_someHelper();
}

// the prototype itself
return {
constructor: A,
method1: method1,
method2: method2
};

})();

var a = new A(10);
var b = new A(20);

a.method1(); // method1: 10
a.method2(); // method2: 10, internal helper: 500

b.method1(); // method1: 20
b.method2(); // method2: 20, internal helper: 500

// both objects are use
// the same methods from
// the same prototype
console.log(a.method1 === b.method1); // true
console.log(a.method2 === b.method2); // true

读写属性

我们提到,属性的读写是由内部的 [[Get]][[Put]] 方法管理。这个方法由属性访问器即操符号和括号符号激活。

1
2
3
4
5
// write
foo.bar = 10; // [[Put]] is called

console.log(foo.bar); // 10, [[Get]] is called
console.log(foo['bar']); // the same

让我们用伪代码来展示这些方法的工作原理。

[[Get]] 方法

[[Get]] 方法也会考虑到来自对象作用域链的属性。因此原型的属性可以作为自己的对象方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
O.[[Get]](P):

// 如果有自己的属性,返回他
if (O.hasOwnProperty(P)) {
return O.P;
}

// 否则分析属性
var __proto = O.[[Prototype]];

// 如果没有原型 (这是可能的,在链的最后,
// Object.prototype.[[Prototype]]等于null),
// 返回undefined;
if (__proto === null) {
return undefined;
}

// 否则,递归调用[[Get]]方法,从原型开始,用过原型链找到原型的属性,
// 此后继续 直到[[Prototype]]等于null
return __proto.[[Get]](P)

注意,因为在某些情况下 [[Get]] 方法返回 undefined ,因此可以检查变量是否存在,像下面这样:

1
2
3
if (window.someObject) {
...
}

这里,属性 someObject 没有在 window 中找到,接着在它的原型找,在原型的原型找,依次递归,在这种情况下,一直没找到,通过算法,返回 undefined 值。

注意,对于确切存在这件事,由 in 操作符负责。他也包含了原型链:

1
2
3
if ('someObject' in window) {
...
}

它有助于避免某些情况,例如 someObject 等于 false 以及即使 someObject 存在也不会通过第一次检测。

[[Put]] 方法

相反,[[Put]] 方法创建或者更新对象的自身属性,并用原型中相同的名字遮盖属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
O.[[Put]](P, V):

// 如果我们不能写入属性,则退出
if (!O.[[CanPut]](P)) {
return;
}

// 如果对象没有这些属性,创建它,所有属性都设置为false
if (!O.hasOwnProperty(P)) {
createNewProperty(O, P, attributes: {
ReadOnly: false,
DontEnum: false,
DontDelete: false,
Internal: false
});
}

// 设置值,如果属性存在,它的属性不能修改
O.P = V

return;

例如:

1
2
3
4
5
6
7
8
9
10
Object.prototype.x = 100;

var foo = {};
console.log(foo.x); // 100, inherited

foo.x = 10; // [[Put]]
console.log(foo.x); // 10, own

delete foo.x;
console.log(foo.x); // again 100, inherited

注意:有可能隐藏内部的只读属性。分配结果将被忽略。这个被内部的 [[CanPut]] 方法控制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// For example, property "length" of
// string objects is read-only; let's make a
// string as a prototype of our object and try
// to shadow the "length" property

function SuperString() {
/* nothing */
}

SuperString.prototype = new String("abc");

var foo = new SuperString();

console.log(foo.length); // 3, the length of "abc"

// try to shadow
foo.length = 5;
console.log(foo.length); // still 3

在ES5的严格模式下,尝试隐藏不可写属性会导致TypeError。

属性访问器

在ECMAScript中,内部的 [[Get]][[Put]] 方法由属性访问器激活,可以通过点符号或者通过括号符号获得。点符号被用于属性名是一个有效的标识符名字以及提前知道的时候,括号符号允许动态生成的属性名称。

1
2
3
4
5
6
7
var a = {testProperty: 10};

console.log(a.testProperty); // 10, dot notation
console.log(a['testProperty']); // 10, bracket notation

var propertyName = 'Property';
console.log(a['test' + propertyName]); // 10, bracket notation with dynamic property

有一个很重要的特征 — 属性访问器总是从属性访问器调用位于左侧的对象的 ToObject 转换。并且由于隐式转换,可能被粗糙的说成“JavaScript一切皆对象”(但是,正如我们早就知道的,当然不是所有的东西,因为还有原始的东西)。

如果我们将属性访问器与原始值一起使用,只需要创建具有相应值的中间包装对象。工作完成后,移除这个包装对象。

🌰:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 10; // 原始值

// 但是它有方法,就像他是一个对象
console.log(a.toString()); // "10"

// moreover, we can even
// (try) to create a new
// property in the "а" primitive calling [[Put]]
// 而且,我们甚至可以尝试在a上创建一个新属性,调用了原始的 [[Put]]
a.test = 100; // 没有报错,这个生效了

// 但是, [[Get]] 方法没有返回这个属性值,而是返回了 undefined
console.log(a.test); // undefined

所以,为什么在这个例子中,原始值 a 可以访问 toString 方法,但是不能访问新创建的 test 属性呢?

答案很简单:

首先,正如我们说的,应用属性访问器后,就不再是原始的,而是中间对象。在这个例子中, new Number(a) 使用了,通过算法找到了作用域链中的 toString 方法。

1
2
3
4
5
// 计算 a.toString() 的算法

1. wrapper = new Number(a);
2. wrapper.toString(); // "10"
3. delete wrapper;

接下来,当计算 test 属性的时候, [[Put]] 方法也会创建自己的中间对象:

1
2
3
4
5
 // 计算 a.test = 100 的算法

1. wrapper = new Number(a);
2. wrapper.test = 100;
3. delete wrapper;

我们看到在第三步中中间对象是被删除了,同时新创建的 test 对象当然会随着对象的移除而移除。

接着,调用 [[Get]] 方法,其中属性访问器再次创建新的包装对象,该包装对象当然对任何 test 属性都不知道:

1
2
3
4
// 计算 a.test 的算法

1. wrapper = new Number(a);
2. wrapper.test; // undefined

也就是说,从原语值引用属性/方法只对读取属性有意义。另外,如果任何一个原始值经常使用对属性的访问,为了节省时间资源,有一种直接用对象表示替换它的感觉。相反,如果值只参与一些不要求访问属性的小计算,则可以使用更有效的原始值。

继承

众所周知,ECMAScript使用基于原型的委托继承。

链接,原型生成时已经提到的原型链。

实际上,所有用域实现委托和原型链分析的工作都简化为上述的 [[Get]] 方法的工作。

如果你完全理解了 [[Get]] 方法的简单算法,Javascript中继承的问题将会自己消失,并且答案将会很清晰明了。

通常在论坛上当谈及关于JavaScript的继承的时候,我只展示一行ECMAScript代码的例子,非常精准明确的描述了这门语言对象的结构,并且展示了基于原型的委托。的确,我们不需要创建任何构造函数或者对象,整个语言都已经继承了。这行代码非常简单:

1
console.log(1..toString()); // "1"

现在我们知道了 [[Get]] 方法和属性访问器的算法,我们可以看看这里发生了什么:

  1. 首先,因为原始值 1new Number(1) 作为中间对象被创建了;
  2. 接着继承的 toString 方法从中间对象调用。

为什么是继承的?因为ECMASCript中对象有自己的属性,并且创建的中间对象,在这里没有自己的 toString 方法。因此,它从原型上继承,即 Number.prototype

注意语法的微妙情况。上面例子中的两个点不是错误,第一个点用于数字的小数部分,第二个点是属性访问器

1
2
3
4
5
6
7
8
9
1.toString(); // SyntaxError!

(1).toString(); // OK

1 .toString(); // OK (space after 1)

1..toString(); // OK

1['toString'](); // OK

原型链

我们来展示如何为用户定义的对象创建作用域链。相当简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function A() {
console.log('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20;

var a = new A();
console.log([a.x, a.y]); // 10 (own), 20 (inherited)

function B() {}

// the easiest variant of prototypes
// chaining is setting child
// prototype to new object created,
// by the parent constructor
B.prototype = new A();

// fix .constructor property, else it would be А
B.prototype.constructor = B;

var b = new B();
console.log([b.x, b.y]); // 10, 20, both are inherited

// [[Get]] b.x:
// b.x (no) -->
// b.[[Prototype]].x (yes) - 10

// [[Get]] b.y
// b.y (no) -->
// b.[[Prototype]].y (no) -->
// b.[[Prototype]].[[Prototype]].y (yes) - 20

// where b.[[Prototype]] === B.prototype,
// and b.[[Prototype]].[[Prototype]] === A.prototype

此方法具有两个特点。

第一, B.prototype 将会包含 x 属性。乍一看,像是不正确,因为 x 属性是作为 A 自身属性定义,并预期在 B 构造函数对象中也是自己的。

在原型继承的情况下,这是正确情况,因为后代对象如果没有这样属于自己的属性,将会委托给原型。背后的想法是,由 B 构造函数创建的对象不需要 x 属性。相反,在基于类模型中,所有的属性都复制到类后代。

但是,如果仍然需要(模拟基于类的方法)由 B 构造函数创建的对象拥有 x 属性,则对此有一些方法,我们将在下面显示其中一种方法。

第二,这已经不是功能而是缺点,当后代原型被创建,构造函数的代码也会执行。我们可以看到 A.[[Call]] activated 消息显示了两次,当 A 构造函数创建的对象被用于 B.prototype 的时候,以及对象创建时 a 对象自身。

一个更关键的例子是父构造函数中抛出的异常:对于这个构造函数创建的实际对象,可能需要这样的检查,但显然,使用这些父对象作为原型是完全不可接受的:

1
2
3
4
5
6
7
8
9
10
11
12
13
function A(param) {
if (!param) {
throw 'Param required';
}
this.param = param;
}
A.prototype.x = 10;

var a = new A(20);
console.log([a.x, a.param]); // 10, 20

function B() {}
B.prototype = new A(); // Error

除此之外,父级构造函数中的大量计算可以视为此方法的缺点。

要解决这些“特点”和问题,现在的程序员使用标准模式来连接原型,我面下面将看到的。这个技巧的主要目标是创建中间包装器构造函数,链接所需的原型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function A() {
console.log('A.[[Call]] activated');
this.x = 10;
}
A.prototype.y = 20;

var a = new A();
console.log([a.x, a.y]); // 10 (own), 20 (inherited)

function B() {
// or simply A.apply(this, arguments)
B.superproto.constructor.apply(this, arguments);
}

// inheritance: chaining prototypes
// via creating empty intermediate constructor
var F = function () {};
F.prototype = A.prototype; // reference
B.prototype = new F();
B.superproto = A.prototype; // explicit reference to ancestor prototype, "sugar"

// fix .constructor property, else it would be A
B.prototype.constructor = B;

var b = new B();
console.log([b.x, b.y]); // 10 (own), 20 (inherited)

注意我们是如何在 b 实例上创建自己的 x 属性的:在新创建对象的上面我中,我们通过 B.superproto.constructor 引用调用父级构造函数。

我们也修复了无须使用父级构造函数来创建后代原型的问题。现在,当需要的时候, "A.[[Call]].activated" 消息会显示。

而且不要每次都重复原型链的相同操作(创建中间构造器,设置 superproto 糖,恢复原来的 constructor 等),这个模板可以封装在方便的工具函数中,不管构造函数的名字是什么,目的都是链接原型:

1
2
3
4
5
6
7
8
function inherit(child, parent) {
var F = function () {};
F.prototype = parent.prototype
child.prototype = new F();
child.prototype.constructor = child;
child.superproto = parent.prototype;
return child;
}

相应的,继承:

1
2
3
4
5
6
7
8
function A() {}
A.prototype.x = 10;

function B() {}
inherit(B, A); // chaining prototypes

var b = new B();
console.log(b.x); // 10, found in the A.prototype

这种包装有很多变种(关于语法方面),但是,所有这些都简化为上述描述。

例如,如果我们将中间构造函数放在外面(因此,只有一个函数会被创建), 可以优化之前的包装,从而重用之前的:

1
2
3
4
5
6
7
8
9
10
var inherit = (function(){
function F() {}
return function (child, parent) {
F.prototype = parent.prototype;
child.prototype = new F;
child.prototype.constructor = child;
child.superproto = parent.prototype;
return child;
};
})();

因为对象的真实的原型是 [[Prototype]] 属性,这意味着 F.prototype 可以被轻易的修改与重用,因为通过 new F 创建的 child.prototype 将会从 child.prototype 的当前值获得他的 [[Prototype]]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function A() {}
A.prototype.x = 10;

function B() {}
inherit(B, A);

B.prototype.y = 20;

B.prototype.foo = function () {
console.log("B#foo");
};

var b = new B();
console.log(b.x); // 10, is found in A.prototype

function C() {}
inherit(C, B);

// and using our "superproto" sugar
// we can call parent method with the same name

C.prototype.foo = function () {
C.superproto.foo.call(this);
console.log("C#foo");
};

var c = new C();
console.log([c.x, c.y]); // 10, 20

c.foo(); // B#foo, C#foo

注意,ES5标准化了这个工具函数,以实现更好的链接。它就是 Object.create 方法。

ES3中的简化版本几乎可以通过以下方式实现:

1
2
3
4
5
6
7
8
9
Object.create ||
Object.create = function (parent, properties) {
function F() {}
F.prototype = parent;
var child = new F;
for (var k in properties) {
child[k] = properties[k].value;
}
return child;

使用:

1
2
3
var foo = {x: 10};
var bar = Object.create(foo, {y: {value: 20}});
console.log(bar.x, bar.y); // 10, 20

关于详情查看这篇文章

同样,“JS中的经典继承”的现有的所有模仿形式都是基于这个原理。现在我们看到,实际上它甚至不是一个“基于模仿类的继承”,而只是原型链简单的代码重用。

注意:在ES6中“class”的概念标准化了,并被实现为如上所述的构造函数之上的语法糖。从这点出发,原型链变成了基于类继承的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ES6
class Foo {
constructor(name) {
this._name = name;
}

getName() {
return this._name;
}
}

class Bar extends Foo {
getName() {
return super.getName() + ' Doe';
}
}

var bar = new Bar('John');
console.log(bar.getName()); // John Doe
文章作者: 踏浪
文章链接: https://www.lyt007.cn/技术/ECMA-262-3深入解析第七章:2、OOP-ECMAScript-实现.html
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 平凡的生活,不平凡的人生
支付宝
微信打赏