DS.Lab筆記 - ECMA-262-3: ECMAScript对于面向对象语言功能的实现

原文鏈接: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


我延伸作者的例子说明下:

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这个关键字。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值