DS.Lab筆記 - ECMA-262-3: ECMAScript对于面向对象语言功能的实现
2017-03-24 16:45
411 查看
原文鏈接:ECMA-262-3 in detail. Chapter 7.2. OOP: ECMAScript implementation.
=============================================================
1.数据类型(Data types)
ECMAScript标准定义了六种可以在代码中直接使用的类型:
Undefined
Null
Number
Boolean
String
Object
还有三种内部类型:
Reference
List
Completion
注:内部类型就是说你的JS代码里不可能出现这三个类型,它们只会在JS解释器内部的实现中出现,而且它们主要是在规范文档中被用来解释和描述语言行为,关于它们,暂时不需要知道太多。
=============================================================
原始数据类型(Primitive value types)
上面的六个里面,有五个是原始的:
Undefined
Null
Number
Boolean
String
我延伸作者的例子说明下:
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
这里出现的一个最大的疑问就是null既然属于自己独有的一个分类:Null,为什么typeof操作符给出的是object?
从实现层面讲,这个结果是在JS解释器里硬编码了的,没什么好说的。从设计理念上讲,作者扒了下历史掌故,ECMAScript标准中没有说明这个地方应该是这么处理,JavaScript的创造者之一Brendan Eich本人也注意到了这个问题,并且是把它作为bug汇报在项目里的,但是大家的共识是先保持现状不做修改,作者的猜测是可能是为了长远的扩展留有空间。
=============================================================
对象类型(Object)
就是指前面的六种里面唯一不是原始类型的那一个。它的定义很简单:一组无序排列的键-值对(key-value pair)。键(key)就被成为属性(property),它的值可以是原始类型的值,也可以是另外的对象类型的值,如果是一个函数对象的话,它也被成为方法(method)。
=============================================================
动态性质(Dynamic nature)
代码已经很一目了然了,注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
另外见附录。
=============================================================
内置对象(built-in objects),本地对象(native objects)和宿主对象(host objects)
太简单了,不如跳过。
=============================================================
布尔对象(Boolean objects),数字对象(Number objects)和字符串对象(String objects)
针对这三个原始类型,标准里定义了将它们转换成对象类型的操作,这种新的对象分别叫:
Boolean-object
String-object
Number-object
实际上它们是用一个对象的内部属性保存了相应的原始数据值,这种对象被成为包装对象(wrapper objects)。
原始类型与相对应的包装对象之间可以转换,如:
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
另外还有一些被內建构造函数创建的对象,诸如:
Function
Math
Date
RegExp
Array
等等。。。。这里我想插一句,有些语言里Array是独立的一种复合数据类型,像那些强类型面向对象语言,所以有些人可能会搞混,在JavaScript里,typeof Array操作返回的是object。
=============================================================
字面标记法
下面的代码,我明白作者想演示的东西,可是在编程中不可能有人会修改內建对象,所以这些知识的价值不是很大,看一下好了:
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
=============================================================
正则表达式
=============================================================
联合数组
在这一节作者多数笔墨都是在澄清一个误传,有些人从其他语言里借鉴来一些数据结构的词汇,用它们来称呼JS里面的对象类型,因为对象是以键-值对的形式存储数据,很多人把它与其他语言里的数据类型混淆了,比如PHP里的联合数组,Python里的字典或者Ruby里的哈希表。其实ECMA标准里面没有定义任何特殊的数据结构类型,就只有一个最基本的对象类型,它能够存储键值对,这个类型的所有子类型也都能做同样操作,比如Function对象类型和Number对象类型。所以作者强调它们是内在一致的,它们其实是同一种类型。
作者更举了下面的例子来证明这种内在一致性:
ECMA里只有属性这样一个通用的概念,并没有区分出索引器,键或者方法这些概念。
=============================================================
类型转换
=============================================================
属性特性(Property attributes)
每个属性也都有一些特性(attribute),比如:
{ReadOnly}
{DontEnum}
{DontDelete}
{Internal}
这里我需要插一点原著没有点拨到的东西,在多数JS的文章里,“property”和“attribute”是有做区分的,property一般就译作属性,借用OOP的概念来说,它通常指的是“对象上的成员变量”,而attribute为了做区分就译作“特性”,其实它一般指的是HTML标签上的属性,比如说div标签可以有name属性:<div name="container"></div>。但是在这里作者说的这个attribute是指property的property,可以理解为成员变量自身的属性,而且它们都是JS解释器内部可见的。
=============================================================
内部属性与方法(Internal properties and methods)
下面列出的内部属性是每个对象都共有的:
[[Prototype]]
[[Class]]
[[Get]]
[[Put]]
[[CanPut]]
[[HasProperty]]
[[Delete]]
[[DefaultValue]]
=============================================================
2.构造函数(Constructor)
构造函数是创建并初始化对象的特殊函数。这节的内容我印象中在函数一章都有讲到,所以先略过,只保留两点强调下:
在构造函数的执行中,也即是在通过new关键字调用的情况下,函数内this是指向新创建的对象的,这个函数默认情况下是返回这个this指向的对象的,可是我们也可以手动返回其它的东西,这样的话,新创建的对象就被丢失了,比如:
在没有参数的情况下,可以不适用( ),直接调用构造函数:
=============================================================
3.原型(Prototype)
每个对象都有个[[Prototype]]属性,它是个隐性的内部属性,不能在JS代码中直接访问到,它的值可以是一个对象或者null。
属性constructor
首先一个来自上一小节的代码例子:
这个标题说的其实是函数对象的[[Prototype]]属性上的那个constructor属性。它指向的是这个函数对象自身,所以这里造成了一个循环引用。由于这点,你可以在新创建的对象上通过它间接地访问到函数对象的[[Prototype]]属性:
显式的prototype和隐式的[[Prototype]]属性
对象上的[[Prototype]]和构造函数上的prototype指向的是同一个对象。在创建对象后,构造函数将自己prototype的值赋给新对象的[[Prototype]]。
作者强调了两点:
对象的[[Prototype]]只在创建过程中被赋值;
对象的[[Prototype]]在创建后不能被更改(这一点我有点不确定,暂且持保留态度,毕竟还没试过,参加下文,通过非正式的__proto__可以),但是对比下,前面作者强调过函数的prototype是可以更改的。
更改构造函数的prototype只会影响其后被创建的对象,在此之前被创建的对象不会受到影响。作者给了个例子:
非标准化的__proto__属性
有一些实现提供了__proto__属性,并且通过它既可以访问也可以更改该对象的原型属性。如:
对象与其构造函数的关系
注:作者删掉一个函数的做法是给它赋值null。
instanceof操作符的特性
它的作用跟函数的prototype属性有一定关联,但是更进一步说,它的操作是严格基于对象的原型链,而不是和用来创建该对象的构造函数有直接联系。这么说是因为函数的原型属性可以被窜改,如果被修改了,那么依然认为用它创建的对象和它有这样的关系就说不通了,instanceof是个二元操作符,左边是要验证的对象,右边是构造函数对象,instanceof的算法其实是看该对象的原型链里面存不存在构造函数对象。所以说,instanceof返回的结果跟这个对象是不是用这个构造函数创建的不存在必然联系,如:
另外还有一点要注意,它触发的是操作符后面那个函数对象上的一个内部属性[[HasInstance]],下面的实验演示了这一点。
用原型来存放共用属性和方法
用例子就足够说明了:
基本上这段代码也演示了在JS里,面相对对象的最基本的实现方式,和最基本的方面:类,实例和数据封装。
注:本小节内的所有代码片段在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中测试的结果都与预期相同。
=============================================================
4.读写属性(Reading and writing properties)
JS里,对于属性的读和写是通过两个内部方法来实现的:[[GET]],[[PUT]],不论是通过.访问属性还是[]。
[[GET]]的算法里包括了对于原型链的查找,而最终如果没能找到所要寻找的变量名,它也只是返回undefined,所以判断一个属性是否存在就不能简单依赖于验证它是否是undefined,因为已经定义了的属性也可以是undefined值。于是用操作符in才是最佳的判断属性存在与否的方法,这个内容在[You
Dont Know JS: This & Object Prototype]第三章里也提到了。
[[PUT]]的算法也不是简单更新属性的值,如果在对象自身上找不到属性名,会自动创建一个属性给它。
注意:在ES5里,不能对于只读属性做更改,甚至是,假如操作对象的原型链上有只读属性,那么也不能在操作对象本身上创建相同名属性来遮蔽它。
最后一个问题就是在基本类型上试用属性访问操作,将会导致这些基本类型变量被自动转换成对应的装裹类(wrapper class),这个内容在[You Dont Know JS: This & Object
Prototype]第三章里也提到了。
=============================================================
5.继承
寻找的过程如下:
[[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
上面代码演示了利用原型链实现继承。可是这个做法有一些弊端,作者的意思表达的不是很清晰,我重新总结下,这个问题就是当继承一个类的时候,必需要实例化父类,也就是一定要调用一次它的构造函数,而其实你只是想给两个类建立继承关系,这个操作本身只是抽象层面的,不一定非要设计切实的数据,但如果恰好父类的构造函数有一些对参数的限制,就会导致这个继承操作失败,比如像下面的情况:
注:Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都测试通过。
作者给出一个解决方案,就是在A与B之间插入一个空壳F,F继承A,B继承F:
首先,我不理解F的必要性,因为解决A构造函数被调用的方法是避免使用new,而是直接操作prototype属性,所以这个地方留待我进一步思考。但是现在需要来验证下这段代码能否解决上面提出的弊端,所以我将代码修改了下:
注:Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都测试通过。
这个做法解决了父类构造函数被执行的问题,另外还带来的一个好处是,B有自己的x属性了,而不是通过原型链找到A那里。
将这个模式封装一下:
使用方法:
我做了下修改:
注:Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都测试通过。
作者还有个进化版,将中间的F函数移出来,测试也是没有问题:
疑问:F是否被共享?
最后,ES5里有Object.create()来解决这个问题,这个要在ES5系列的文章里再细说了。
作者也给出了对没有支持ES5的打包的代码:
注:在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都给出异常Uncaught ReferenceError: Invalid left-hand side in assignment。
ES6更是规范化了class这个关键字。
=============================================================
1.数据类型(Data types)
ECMAScript标准定义了六种可以在代码中直接使用的类型:
Undefined
Null
Number
Boolean
String
Object
还有三种内部类型:
Reference
List
Completion
注:内部类型就是说你的JS代码里不可能出现这三个类型,它们只会在JS解释器内部的实现中出现,而且它们主要是在规范文档中被用来解释和描述语言行为,关于它们,暂时不需要知道太多。
=============================================================
原始数据类型(Primitive value types)
上面的六个里面,有五个是原始的:
Undefined
Null
Number
Boolean
String
我延伸作者的例子说明下:
var a = undefined; var b = null; var c = true; var d = 'test'; var e = 10; console.log(typeof a); // undefined console.log(typeof b); // object console.log(typeof c); // boolean console.log(typeof d); // string console.log(typeof e); // number
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
这里出现的一个最大的疑问就是null既然属于自己独有的一个分类:Null,为什么typeof操作符给出的是object?
console.log(typeof null); // "object"
从实现层面讲,这个结果是在JS解释器里硬编码了的,没什么好说的。从设计理念上讲,作者扒了下历史掌故,ECMAScript标准中没有说明这个地方应该是这么处理,JavaScript的创造者之一Brendan Eich本人也注意到了这个问题,并且是把它作为bug汇报在项目里的,但是大家的共识是先保持现状不做修改,作者的猜测是可能是为了长远的扩展留有空间。
=============================================================
对象类型(Object)
就是指前面的六种里面唯一不是原始类型的那一个。它的定义很简单:一组无序排列的键-值对(key-value pair)。键(key)就被成为属性(property),它的值可以是原始类型的值,也可以是另外的对象类型的值,如果是一个函数对象的话,它也被成为方法(method)。
=============================================================
动态性质(Dynamic nature)
var foo = {x: 10}; // add new property foo.y = 20; console.log(foo); // Object {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); // Object {y: 20}
代码已经很一目了然了,注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
另外见附录。
=============================================================
内置对象(built-in objects),本地对象(native objects)和宿主对象(host objects)
太简单了,不如跳过。
=============================================================
布尔对象(Boolean objects),数字对象(Number objects)和字符串对象(String objects)
针对这三个原始类型,标准里定义了将它们转换成对象类型的操作,这种新的对象分别叫:
Boolean-object
String-object
Number-object
实际上它们是用一个对象的内部属性保存了相应的原始数据值,这种对象被成为包装对象(wrapper objects)。
原始类型与相对应的包装对象之间可以转换,如:
var c = new Boolean(true); var d = new String('test'); var e = new Number(10); console.log(typeof c); console.log(typeof d); console.log(typeof e); // converting to primitive // conversion: ToPrimitive // applying as a function, without "new" keyword c = Boolean(c); d = String(d); e = Number(e); console.log(typeof c); console.log(typeof d); console.log(typeof e); // back to Object // conversion: ToObject c = Object(c); d = Object(d); e = Object(e); console.log(typeof c); console.log(typeof d); console.log(typeof e);
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
另外还有一些被內建构造函数创建的对象,诸如:
Function
Math
Date
RegExp
Array
等等。。。。这里我想插一句,有些语言里Array是独立的一种复合数据类型,像那些强类型面向对象语言,所以有些人可能会搞混,在JavaScript里,typeof Array操作返回的是object。
=============================================================
字面标记法
// 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;
下面的代码,我明白作者想演示的东西,可是在编程中不可能有人会修改內建对象,所以这些知识的价值不是很大,看一下好了:
var getClass = Object.prototype.toString; Object = Number; var foo = new Object; console.log([foo, getClass.call(foo)]); // [Number, "[object Number]"] var bar = {}; console.log([bar, getClass.call(bar)]); // [Object, "[object Object]"] Array = Number; foo = new Array; console.log([foo, getClass.call(foo)]); // [Number, "[object Number]"] bar = []; console.log([bar, getClass.call(bar)]); // [Array[0], "[object Array]"] RegExp = Number; foo = new RegExp; console.log([foo, getClass.call(foo)]); // [Number, "[object Number]"] bar = /(?!)/g; console.log([bar, getClass.call(bar)]); // [/(?!)/g, "[object RegExp]"]
注:這段代碼在Chrome (Version 56.0.2924.87)和Firefox (51.0.1 (32-bit))中都測試過。
=============================================================
正则表达式
=============================================================
联合数组
在这一节作者多数笔墨都是在澄清一个误传,有些人从其他语言里借鉴来一些数据结构的词汇,用它们来称呼JS里面的对象类型,因为对象是以键-值对的形式存储数据,很多人把它与其他语言里的数据类型混淆了,比如PHP里的联合数组,Python里的字典或者Ruby里的哈希表。其实ECMA标准里面没有定义任何特殊的数据结构类型,就只有一个最基本的对象类型,它能够存储键值对,这个类型的所有子类型也都能做同样操作,比如Function对象类型和Number对象类型。所以作者强调它们是内在一致的,它们其实是同一种类型。
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;
作者更举了下面的例子来证明这种内在一致性:
var a = new String("foo"); a['length'] = 10; console.log(a['length']); // 3
ECMA里只有属性这样一个通用的概念,并没有区分出索引器,键或者方法这些概念。
=============================================================
类型转换
=============================================================
属性特性(Property attributes)
每个属性也都有一些特性(attribute),比如:
{ReadOnly}
{DontEnum}
{DontDelete}
{Internal}
这里我需要插一点原著没有点拨到的东西,在多数JS的文章里,“property”和“attribute”是有做区分的,property一般就译作属性,借用OOP的概念来说,它通常指的是“对象上的成员变量”,而attribute为了做区分就译作“特性”,其实它一般指的是HTML标签上的属性,比如说div标签可以有name属性:<div name="container"></div>。但是在这里作者说的这个attribute是指property的property,可以理解为成员变量自身的属性,而且它们都是JS解释器内部可见的。
=============================================================
内部属性与方法(Internal properties and methods)
下面列出的内部属性是每个对象都共有的:
[[Prototype]]
[[Class]]
[[Get]]
[[Put]]
[[CanPut]]
[[HasProperty]]
[[Delete]]
[[DefaultValue]]
=============================================================
2.构造函数(Constructor)
构造函数是创建并初始化对象的特殊函数。这节的内容我印象中在函数一章都有讲到,所以先略过,只保留两点强调下:
在构造函数的执行中,也即是在通过new关键字调用的情况下,函数内this是指向新创建的对象的,这个函数默认情况下是返回这个this指向的对象的,可是我们也可以手动返回其它的东西,这样的话,新创建的对象就被丢失了,比如:
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]
在没有参数的情况下,可以不适用( ),直接调用构造函数:
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
=============================================================
3.原型(Prototype)
每个对象都有个[[Prototype]]属性,它是个隐性的内部属性,不能在JS代码中直接访问到,它的值可以是一个对象或者null。
属性constructor
首先一个来自上一小节的代码例子:
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]]属性上的那个constructor属性。它指向的是这个函数对象自身,所以这里造成了一个循环引用。由于这点,你可以在新创建的对象上通过它间接地访问到函数对象的[[Prototype]]属性:
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
显式的prototype和隐式的[[Prototype]]属性
对象上的[[Prototype]]和构造函数上的prototype指向的是同一个对象。在创建对象后,构造函数将自己prototype的值赋给新对象的[[Prototype]]。
作者强调了两点:
对象的[[Prototype]]只在创建过程中被赋值;
对象的[[Prototype]]在创建后不能被更改(这一点我有点不确定,暂且持保留态度,毕竟还没试过,参加下文,通过非正式的__proto__可以),但是对比下,前面作者强调过函数的prototype是可以更改的。
更改构造函数的prototype只会影响其后被创建的对象,在此之前被创建的对象不会受到影响。作者给了个例子:
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
非标准化的__proto__属性
有一些实现提供了__proto__属性,并且通过它既可以访问也可以更改该对象的原型属性。如:
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
对象与其构造函数的关系
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 both implicit references delete a.constructor.prototype.constructor; delete b.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
注:作者删掉一个函数的做法是给它赋值null。
instanceof操作符的特性
它的作用跟函数的prototype属性有一定关联,但是更进一步说,它的操作是严格基于对象的原型链,而不是和用来创建该对象的构造函数有直接联系。这么说是因为函数的原型属性可以被窜改,如果被修改了,那么依然认为用它创建的对象和它有这样的关系就说不通了,instanceof是个二元操作符,左边是要验证的对象,右边是构造函数对象,instanceof的算法其实是看该对象的原型链里面存不存在构造函数对象。所以说,instanceof返回的结果跟这个对象是不是用这个构造函数创建的不存在必然联系,如:
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
另外还有一点要注意,它触发的是操作符后面那个函数对象上的一个内部属性[[HasInstance]],下面的实验演示了这一点。
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
用原型来存放共用属性和方法
用例子就足够说明了:
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
基本上这段代码也演示了在JS里,面相对对象的最基本的实现方式,和最基本的方面:类,实例和数据封装。
注:本小节内的所有代码片段在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中测试的结果都与预期相同。
=============================================================
4.读写属性(Reading and writing properties)
JS里,对于属性的读和写是通过两个内部方法来实现的:[[GET]],[[PUT]],不论是通过.访问属性还是[]。
[[GET]]的算法里包括了对于原型链的查找,而最终如果没能找到所要寻找的变量名,它也只是返回undefined,所以判断一个属性是否存在就不能简单依赖于验证它是否是undefined,因为已经定义了的属性也可以是undefined值。于是用操作符in才是最佳的判断属性存在与否的方法,这个内容在[You
Dont Know JS: This & Object Prototype]第三章里也提到了。
[[PUT]]的算法也不是简单更新属性的值,如果在对象自身上找不到属性名,会自动创建一个属性给它。
注意:在ES5里,不能对于只读属性做更改,甚至是,假如操作对象的原型链上有只读属性,那么也不能在操作对象本身上创建相同名属性来遮蔽它。
最后一个问题就是在基本类型上试用属性访问操作,将会导致这些基本类型变量被自动转换成对应的装裹类(wrapper class),这个内容在[You Dont Know JS: This & Object
Prototype]第三章里也提到了。
=============================================================
5.继承
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
上面代码演示了利用原型链实现继承。可是这个做法有一些弊端,作者的意思表达的不是很清晰,我重新总结下,这个问题就是当继承一个类的时候,必需要实例化父类,也就是一定要调用一次它的构造函数,而其实你只是想给两个类建立继承关系,这个操作本身只是抽象层面的,不一定非要设计切实的数据,但如果恰好父类的构造函数有一些对参数的限制,就会导致这个继承操作失败,比如像下面的情况:
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
注:Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都测试通过。
作者给出一个解决方案,就是在A与B之间插入一个空壳F,F继承A,B继承F:
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)
首先,我不理解F的必要性,因为解决A构造函数被调用的方法是避免使用new,而是直接操作prototype属性,所以这个地方留待我进一步思考。但是现在需要来验证下这段代码能否解决上面提出的弊端,所以我将代码修改了下:
function A(param) { if (!param) { throw 'Param required'; } this.x = param; } A.prototype.y = 20; var a = new A(100); console.log([a.x, a.y]); // function B() { B.superproto.constructor.apply(this, arguments); } var F = function () {}; F.prototype = A.prototype; B.prototype = new F(); B.superproto = A.prototype; B.prototype.constructor = B; var b = new B(10); console.log([b.x, b.y]);
注:Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都测试通过。
这个做法解决了父类构造函数被执行的问题,另外还带来的一个好处是,B有自己的x属性了,而不是通过原型链找到A那里。
将这个模式封装一下:
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; }
使用方法:
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
我做了下修改:
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; } function A(param) { if (!param) { throw 'Param required'; } this.x = param; } A.prototype.y = 10; function B(){ B.superproto.constructor.apply(this, arguments); } inherit(B, A); // chaining prototypes var b = new B(34); console.log(b.x, b.y); // 34, 10
注:Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都测试通过。
作者还有个进化版,将中间的F函数移出来,测试也是没有问题:
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; }; })(); 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
疑问:F是否被共享?
最后,ES5里有Object.create()来解决这个问题,这个要在ES5系列的文章里再细说了。
作者也给出了对没有支持ES5的打包的代码:
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; } var foo = {x: 10}; var bar = Object.create(foo, {y: {value: 20}}); console.log(bar.x, bar.y); // 10, 20
注:在Chrome Version 57.0.2987.133 (64-bit) 和Firefox Version 52.0.2 (32-bit)中都给出异常Uncaught ReferenceError: Invalid left-hand side in assignment。
ES6更是规范化了class这个关键字。
相关文章推荐
- DS.Lab筆記 - ECMA-262-3: 闭包
- DS.Lab筆記 - ECMA-262-5 - 属性与属性描述器
- DS.Lab筆記 - ECMA-262-5 - 严谨模式
- DS.Lab筆記 - ECMA-262-3: 函数
- DS.Lab筆記 - ECMA-262-3: 變量體(variable object)
- DS.Lab筆記 - ECMA-262-3: this
- DS.Lab筆記 - ECMA-262-5 - 文法环境概论
- DS.Lab筆記 - ECMA-262-3: 作用域链
- DS.Lab筆記 - ECMA-262-3: 求值策略
- Sharepoint学习笔记—ECMAScript对象模型系列-- 2、实现编写代码时的智能提示功能
- 面向对象(实现相应功能操作)
- SharePoint【ECMAScript对象模型系列】-- 02. 实现编写代码时的智能提示功能
- 面向对象语言中实现多态方法总结
- Sharepoint学习笔记—ECMAScript对象模型系列-- 2、实现编写代码时的智能提示功能
- 转载:面向对象语言中实现多态方法
- [嵌入式开发模块]机械按钮模块 纯C语言 面向对象实现 按键消抖、长按、连击
- 用Java面向对象思想实现一个微博的功能(未完)
- Sharepoint学习笔记—ECMAScript对象模型--实现编写代码时的智能提示功能
- Sharepoint学习笔记―ECMAScript对象模型--实现编写代码时的智能提示功能
- JS面向对象应用一(界面分离JS,多语言及校验组件实现)