zoukankan      html  css  js  c++  java
  • OOP—ECMAScript实现详解

    我们将从最基本的数据类型来分析,首先要了解的是ECMAScript用原始值( primitive values 和对象

    objects 来区分实体, 因此有些文章里说的“在JavaScript里, 一切都是对象”是错误的( 不完全对)

    始值就是我们这里要讨论的一些数据类型。

    数据类型

    大家都知道ECMAScript是可以动态转换类型的动态弱类型语言,即便如此,它还是有数据类型的。在标准中定义了9种数据类型,但只有6种是我们可以直接在ECMAScript程序里访问的。分别是:NumberStringBooleanObjectNullUndefined

    另外三种只能在实现级别访问:ReferenceListCompletion。其中,Reference是用来解释delete typeof this这样的操作符, 并且包含一个基对象和一个属性名称;List描述的是参数列表的行为( new表达式和函数调用的时候) Completion是用来解释行为breakcontinue returnthrow语句的。

     

     

    原始值类型

    在前面我们提到过的6种克制在ECMAScript程序中直接访问的数据类型中,有5种是原始值类型,分别是:

    NumberStringBooleanNullUndefined。原始值类型例子:

    var a = 10;
    
    var b = 'string';
    
    var c = true;
    
    var d = null;
    
    var e = undefined;

     

    这些值是底层直接实现的,不是Object。它们没有构造函数,没有原型。

    注:这些原始值和我们平时用的(Boolean String Number Object)虽然名字上相似, 但不是同一个

    东西。 所以typeof(true)typeof(Boolean)结果是不一样的, 因为typeof(Boolean)的结果是"function" 所以

    函数Boolean String Number是有原型的

    至于 typeof(null)返回"Object",规范中并没有作很多的解释,只是如此规定:对于Null值的typeof字符串返回值返回"Object"

    规范没有想解释这个, 但是Brendan Eich (JavaScript发明人)注意到null相对于undefined大多数都是用

    于对象出现的地方, 例如设置一个对象为空引用。 但是有些文档里有些气人将之归结为bug 而且将该bug

    放在Brendan Eich也参与讨论的bug列表里, 结果就是任其自然, 还是把typeof null的结果设置为

    object 尽管262-3的标准是定义null的类型是Null 262-5已经将标准修改为null的类型是object了)。

     

     

    Object类型

    Object类型是描述ECMAScript对象唯一一个数据类型。(不要和Object构造器混淆了,这里只讨论抽象类型) 那么什么是对象呢: Object is an unordered collection of key-value pairs.(对象是一个包含 key-value 对的无序集合)。其中对象的key称之为属性,属性是原始值和其他对象的容器。若函数的属性为一个函数我们则称之为方法。例如:

    var obj = {
    
    a:1,
    
    b:{b1:false},
    
    c:function(){
    
    console.log('oop')
    
    }
    
    }

    动态性

    ECMAScript中对象是完全动态的,这意味着在程序执行期间,我们可以任意的添加、修改、删除对象的属性。例如:

    var obj = {a:1};
    
     
    
    //添加属性
    
    obj.b = {b1:true};
    
    //修改属性
    
    obj.a = 'test';
    
    //删除属性
    
    delete obj.a;

    有些属性不能被修改——( 只读属性、 已删除属性或不可配置的属性) 我们将稍后在属性特性里讲解。

    另外, ECMAScript5规范规定, 静态对象不能扩展新的属性, 并且它的属性页不能删除或者修改。 他们是所谓的冻

    结对象, 可以通过应用Object.freeze(o)方法得到。

     

    var foo = {x:6};
    
     
    
    //冻结foo
    
    Object.freeze(foo);
    
    console.log(Object.isFrozen(foo));//true
    
     
    
    //不能修改
    
    foo.x = 200;
    
    console.log(foo.x);//6
    
     
    
    //不能添加
    
    foo.y = false;
    
    console.log(foo.y);//undefined
    
     
    
    //不能删除
    
    delete foo.x;//false

    ECMAScript5规范里, 也使用Object.preventExtensions(o)方法防止扩展, 或者使用Object.defineProperty(o)

    法来定义属性:

     

    内置对象、原生对象宿主对象

    有必要注意的是。规范区分了 内置对象、 元素对象宿主对象。

    内置对象和元素对象是被ECMAScript规范定义和实现的, 两者之间的差异微不足道。 所有ECMAScript

    现的对象都是原生对象( 其中一些是内置对象、 一些在程序执行的时候创建, 例如用户自定义对象)

    置对象是原生对象的一个子集、 是在程序开始之前内置到ECMAScript里的( 例如, parseInt, Match等)

    所有的宿主对象是由宿主环境提供的, 通常是浏览器, 并可能包括如window alert等。

    注意, 宿主对象可能是ECMAScript自身实现的, 完全符合规范的语义。 从这点来说, 他们能称为“原生宿主”对象 尽快很理论) 不过规范没有定义“原生宿主”对象的概念。

    var foo = {x : 10};
    
    Object.defineProperty(foo, "y", {
    
    value: 20,
    
    writable: false, // 只读
    
    configurable: false // 不可配置
    
    });
    
    // 不能修改
    
    foo.y = 200;
    
    // 不能删除
    
    delete foo.y; // false
    
    // 防治扩展
    
    Object.preventExtensions(foo);
    
    console.log(Object.isExtensible(foo)); // false
    
    // 不能添加新属性
    
    foo.z = 30;
    
    console.log(foo); //{x: 10, y: 20}

    BooleanNumber and String

     

    此外,规范还定义了一些原生的特殊包装类:布尔对象、数字对象和字符串对象。

    这些对象的创建, 是通过相应的内置构造器创建, 并且包含原生值作为其内部属性, 这些对象可以转换

    原始值, 反之亦然。

    var c = new Boolean(true);
    
    var d = new String('test');
    
    var e = new Number(10);
    
    // 转换成原始值
    
    // 使用不带new关键字的函数
    
    с = Boolean(c);
    
    d = String(d);
    
    e = Number(e);
    
    // 重新转换成对象
    
    с = Object(c);
    
    d = Object(d);
    
    e = Object(e);

    此外, 也有对象是由特殊的内置构造函数创建: Function 函数对象构造器) Array 数组构造器)

    RegExp 正则表达式构造器) Math 数学模块) Date 日期的构造器) 等等, 这些对象也是Object

    对象类型的值, 他们彼此的区别是由内部属性管理的。

    字面量Literal

    对于三个对象的值:对象( object ,数组( array 和正则表达式( regular expression 他们分别有简写

    的标示符称为:对象初始化器、 数组初始化器、 和正则表达式初始化器:

    // 等价于new Array(1, 2, 3);
    
    // 或者array = new Array();
    
    // array[0] = 1;
    
    // array[1] = 2;
    
    // array[2] = 3;
    
    var array = [1, 2, 3];
    
    // 等价于
    
    // var object = new Object();
    
    // object.a = 1;
    
    // object.b = 2;
    
    // object.c = 3;
    
    var object = {a: 1, b: 2, c: 3};
    
    // 等价于new RegExp("^d+$", "g")
    
    var re = /^d+$/g;

    正则表达式字面量和RegExp对象

    在第三版的规范里,正则表达式字面量和RegExp对象有如下问题:

    RegExp字面量只在一句里存在, 且再解析阶段创建, RegExp构造器创建的却是新对象, 所以这可能会导致出一些问题, lastIndex的值

    在测试的时候结果是错误的:

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

    注意:此问题在262-5中得到修正,不管是字面量还是通过RegExp构造器形式,都会创建新对象。

    另外, ECMAScript5标准可以让我们创建没原型的对象( 使用Object.create(null)方法实现) 对, 从这个角度来

    说, 这样的对象可以称之为哈希表:

    var aHashTable = Object.create(null);
    
    console.log(aHashTable.toString); // 未定义

     

    对象转换

    将对象转化成原始值可以用valueOf方法, 正如我们所说的, 当函数的构造函数调用做为function 对于某

    些类型的) 但如果不用new关键字就是将对象转化成原始值, 就相当于隐式的valueOf方法调用:

    var a = new Number(1);
    
    var primitiveA = Number(a); // 隐式"valueOf"调用
    
    var alsoPrimitiveA = a.valueOf(); // 显式调用
    
    alert([
    
    typeof a, // "object"
    
    typeof primitiveA, // "number"
    
    typeof alsoPrimitiveA // "number"
    
    ]);
    
    这种方式允许对象参与各种操作, 例如:
    
    var a = new Number(1);
    
    var b = new Number(2);
    
    alert(a + b); // 3
    
    // 甚至
    
    var c = {
    
    x: 10,
    
    y: 20,
    
    valueOf: function () {
    
    return this.x + this.y;
    
    }
    
    };
    
    var d = {
    
    x: 30,
    
    y: 40,
    
    // 和c的valueOf功能一样
    
    valueOf: c.valueOf
    
    };
    
    alert(c + d); // 100

    valueOf的默认值会根据根据对象的类型改变( 如果不被覆盖的话) 对某些对象, 他返回的是this——

    如:Object.prototype.valueOf() 还有计算型的值:Date.prototype.valueOf()返回的是日期时间:

    var a = {};
    
    alert(a.valueOf() === a); // true, "valueOf"返回this
    
    var d = new Date();
    
    alert(d.valueOf()); //current  time
    
    alert(d.valueOf() === d.getTime()); // true

    此外,对象还有一个更原始的代表性——字符串展示。 这个toString方法是可靠的, 它在某些操作上是自动

    使用的:

    var a = {
    
    valueOf: function () {
    
    return 100;
    
    },
    
    toString: function () {
    
    return 'test';
    
    }
    
    };

    // 这个操作里, toString方法自动调用

    alert(a); // "test"

    // 但是这里, 调用的却是valueOf()方法

    alert(a + 10); // 110

    // 但, 一旦valueOf删除以后

    // toString又可以自动调用了

    delete a.valueOf;

    alert(a + 10); // "test10"

    Object.prototype上定义的toString方法具有特殊意义, 它返回的我们下面将要讨论的内部[[Class]]属性值。

    和转化成原始值( ToPrimitive 相比, 将值转化成对象类型也有一个转化规范( ToObject

    一个显式方法是使用内置的Object构造函数作为function来调用ToObject 有些类似通过new关键字也可

    以)

    var n = Object(1); // [object Number]

    var s = Object('test'); // [object String]

    // 一些类似, 使用new操作符也可以

    var b = new Object(true); // [object Boolean]

    // 应用参数new Object的话创建的是简单对象

    var o = new Object(); // [object Object]

    // 如果参数是一个现有的对象

    // 那创建的结果就是简单返回该对象

    var a = [];

    alert(a === new Object(a)); // true

    alert(a === Object(a)); // true

    关于调用内置构造函数, 适用还是不适用new操作符没有通用规则, 取决于构造函数。 例如Array

    Function当使用new操作符的构造函数或者不使用new操作符的简单函数使用产生相同的结果的:

    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]

    属性的特性

    所有的属性( property 都可以有很多特性( attributes

    1. {Writable}——是否忽略向属性赋值的写操作尝, 但只读属性可以由宿主环境行为改变——也就是说不

    是“恒定值” ;

    2. {Enumerable}——设置属性是否能被for..in循环枚举

    3. {Configurable}— 是否忽略delete操作符的行为( 即删不掉) ;

    4. {Internal}——内部属性, 没有名字( 仅在实现层面使用) ECMAScript里无法访问这样的属性。

    内部属性和方法

    对象也可以有内部属性( 实现层面的一部分) 并且ECMAScript程序无法直接访问( 但是下面我们将看

    到, 一些实现允许访问一些这样的属性) 这些属性通过嵌套的中括号[[ ]]进行访问。 我们来看其中的一

    些, 这些属性的描述可以到规范里查阅到。

    每个对象都应该实现如下内部属性和方法:

    1. [[Prototype]]——对象的原型( 将在下面详细介绍)

    2. [[Class]]——字符串对象的一种表示( 例如, Object Array Function Object Function等) ;用来区

    分对象

    3. [[Get]]——获得属性值的方法

    4. [[Put]]——设置属性值的方法

    5. [[CanPut]]——检查属性是否可写

    6. [[HasProperty]]——检查对象是否已经拥有该属性

    7. [[Delete]]——从对象删除该属性

    8. [[DefaultValue]]返回对象对应的原始值( 调用valueOf方法, 某些对象可能会抛出TypeError异常)

    通过Object.prototype.toString()方法可以间接得到内部属性[[Class]]的值, 该方法应该返回下列字符串: "

    [object " + [[Class]] + "]" 例如:

    var getClass = Object.prototype.toString;
    
    getClass.call({}); // [object Object]
    
    getClass.call([]); // [object Array]
    
    getClass.call(new Number(1)); // [object Number]

    // 等等

     

    构造函数

    ECMAScript中的对象是通过所谓的构造函数来创建的。

    Constructor is a function that creates and initializes the newly created object.(构造函数是一个函数, 用来创建并初始化新创建的对象。)

    对象创建( 内存分配) 是由构造函数的内部方法[[Construct]]负责的。 该内部方法的行为是定义好的, 所有

    的构造函数都是使用该方法来为新对象分配内存的。

    而初始化是通过新建对象上下上调用该函数来管理的, 这是由构造函数的内部方法[[Call]]来负责任的。

    注意, 用户代码只能在初始化阶段访问, 虽然在初始化阶段我们可以返回不同的对象( 忽略第一阶段创建

    this对象)

    function A() {
    
    // 更新新创建的对象
    
    this.x = 10;
    
    // 但返回的是不同的对象
    
    return [1, 2, 3];
    
    } v
    
    ar a = new A();
    
    console.log(a.x, a); undefined, [1, 2, 3]

     

    对象创建的算法

    内部方法[[Construct]] 的行为可以描述成如下:

    F.[Construct]:
    
    O = new NativeObject();
    
    // 属性[[Class]]被设置为"Object"
    
    O.[[Class]] = "Object"
    
    // 引用F.prototype的时候获取该对象g
    
    var objectPrototype = F.prototype;
    
    // 如果objectPrototype是对象, 就:
    
    O.[[Prototype]] = __objectPrototype
    
    // 否则:
    
    O.[[Prototype]] = Object.prototype;
    
    // 这里O.[[Prototype]]是Object对象的原型
    
    // 新创建对象初始化的时候应用了F.[[Call]]
    
    // 将this设置为新创建的对象O
    
    // 参数和F里的initialParameters是一样的
    
    R = F.[Call]; this === O;
    
    // 这里R是[[Call]]的返回值
    
    // 在JS里看, 像这样:
    
    // R = F.apply(O, initialParameters);
    
    // 如果R是对象
    
    return R
    
    // 否则
    
    return O

    请注意两个主要特点:

    1. 首先, 新创建对象的原型是从当前时刻函数的prototype属性获取的( 这意味着同一个构造函数创建的

    两个对象的原型可以不同,因为函数的prototype属性可以不同)

    2. 其次, 正如我们上面提到的, 如果在对象初始化的时候, [[Call]]返回的是对象, 这恰恰是用于整个new

    操作符的结果:

    function A() {}

    A.prototype.x = 10;

    var a = new A(); alert(a.x); // 10 – 从原型上得到

     // 设置.prototype属性为新对象

    // 为什么显式声.constructor属性将在下面说明

    A.prototype = { constructor: A, y: 100 };

    var b = new A(); // 对象"b"有了新属性 alert(b.x);

     // undefined alert(b.y);

     // 100 – 从原型上得到

    // a对象的原型依然可以得到原来的结果 alert(a.x); // 10 - 从原型上得到 function B() { this.x = 10; return new

    Array(); }

    // 如果"B"构造函数没有返回( 或返回this // 那么this对象就可以使用, 但是下面的情况返回的是array var

    对象创建的算法

    b = new B(); alert(b.x); // undefined alert(Object.prototype.toString.call(b)); // [object Array]

    让我们来详细了解一下原型

    原型

    每个对象都有一个原型( 一些系统对象除外) 原型通信是通过内部的、 隐式的、 不可直接访问

    [[Prototype]]原型属性来进行的, 原型可以是一个对象, 也可以是null值。

    instanceof操作符的特性

    我们是通过构造函数的prototype属性来显示引用原型的,这和instanceof操作符有关。该操作符是和原型链一起工作的,而不是构造函数,考虑到这一点,当检测对象的时候往往会有误解:

    if (foo instanceof Foo) {
    ...
    }

    这不是用来检测对象foo是否是用Foo构造函数创建的,所有instanceof运算符只需要一个对象属性——foo.[[Prototype]],在原型链中从Foo.prototype开始检查其是否存在。instanceof运算符是通过构造函数里的内部方法[[HasInstance]]来激活的。

    让我们来看看这个例子:

    function A() {}
    A.prototype.x = 10;

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

    alert(a instanceof A); // true

    // 如果设置原型为null
    A.prototype = null;

    // ..."a"依然可以通过a.[[Prototype]]访问原型
    alert(a.x); // 10

    // 不过,instanceof操作符不能再正常使用了
    // 因为它是从构造函数的prototype属性来实现的
    alert(a instanceof A); // 错误,A.prototype不是对象

    另一方面,可以由构造函数来创建对象,但如果对象的[[Prototype]]属性和构造函数的prototype属性的值设置的是一样的话,instanceof检查的时候会返回true:

    function B() {}
    var b = new B();

    alert(b instanceof B); // true

    function C() {}

    var __proto = {
    constructor: C
    };

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

    alert(b instanceof C); // true
    alert(b instanceof B); // false

    原型可以存放方法并共享属性

    大部分程序里使用原型是用来存储对象的方法、默认状态和共享对象的属性。

    事实上,对象可以拥有自己的状态 ,但方法通常是一样的。 因此,为了内存优化,方法通常是在原型里定义的。 这意味着,这个构造函数创建的所有实例都可以共享找个方法。

    function A(x) {
      this.x = x || 100;
    }
     
    A.prototype = (function () {
     
      // 初始化上下文
      // 使用额外的对象
     
      var _someSharedVar = 500;
     
      function _someHelper() {
        alert('internal helper: ' + _someSharedVar);
      }
     
      function method1() {
        alert('method1: ' + this.x);
      }
     
      function method2() {
        alert('method2: ' + this.x);
        _someHelper();
      }
     
      // 原型自身
      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
     
    // 2个对象使用的是原型里相同的方法
    alert(a.method1 === b.method1); // true
    alert(a.method2 === b.method2); // true

    读写属性

    正如我们提到,读取和写入属性值是通过内部的[[Get]]和[[Put]]方法。这些内部方法是通过属性访问器激活的:点标记法或者索引标记法:

    // 写入
    foo.bar = 10; // 调用了[[Put]]
     
    console.log(foo.bar); // 10, 调用了[[Get]]
    console.log(foo['bar']); // 效果一样

    下面,我们来看看伪代码实现:

    [[Get]]方法

    [Get]]也会从原型链中查询属性,所以通过对象也可以访问原型中的属性。

    O.[[Get]](P):
     
    // 如果是自己的属性,就返回
    if (O.hasOwnProperty(P)) {
      return O.P;
    }
     
    // 否则,继续分析原型
    var __proto = O.[[Prototype]];
     
    // 如果原型是null,返回undefined
    // 这是可能的:最顶层Object.prototype.[[Prototype]]是null
    if (__proto === null) {
      return undefined;
    }
     
    // 否则,对原型链递归调用[[Get]],在各层的原型中查找属性
    // 直到原型为null
    return __proto.[[Get]](P)

    请注意,因为[[Get]]在如下情况也会返回undefined:

    if (window.someObject) {
    ...
    }

    这里,在window里没有找到someObject属性,然后会在原型里找,原型的原型里找,以此类推,如果都找不到,按照定义就返回undefined。

    注意:in操作符也可以负责查找属性(也会查找原型链):

    if ('someObject' in window) {
    ...
    }

    这有助于避免一些特殊问题:比如即便someObject存在,在someObject等于false的时候,第一轮检测就通不过。

    [[PUT]]方法

    [[Put]]方法可以创建、更新对象自身的属性,并且掩盖原型里的同名属性。

    O.[[Put]](P, V):
     
    // 如果不能给属性写值,就退出
    if (!O.[[CanPut]](P)) {
      return;
    }
     
    // 如果对象没有自身的属性,就创建它
    // 所有的attributes特性都是false
    if (!O.hasOwnProperty(P)) {
      createNewProperty(O, P, attributes: {
        ReadOnly: false,
        DontEnum: false,
        DontDelete: false,
        Internal: false
      });
    }
     
    // 如果属性存在就设置值,但不改变attributes特性
    O.P = V
     
    return;

    例如:

    Object.prototype.x = 100;
     
    var foo = {};
    console.log(foo.x); // 100, 继承属性
     
    foo.x = 10; // [[Put]]
    console.log(foo.x); // 10, 自身属性
     
    delete foo.x;
    console.log(foo.x); // 重新是100,继承属性

    请注意,不能掩盖原型里的只读属性,赋值结果将忽略,这是由内部方法[[CanPut]]控制的。

    // 例如,属性length是只读的,我们来掩盖一下length试试
     
    function SuperString() {
      /* nothing */
    }
     
    SuperString.prototype = new String("abc");
     
    var foo = new SuperString();
     
    console.log(foo.length); // 3, "abc"的长度
     
    // 尝试掩盖
    foo.length = 5;
    console.log(foo.length); // 依然是3

    在ECMAScript5的严格模式下,如果掩盖只读属性的话,会保存TypeError错误。

    属性访问器

    内部方法[[Get]]和[[Put]]在ECMAScript里是通过点符号或者索引法来激活的,如果属性标示符是合法的名字的话,可以通过“.”来访问,而索引方运行动态定义名称。

    var a = {testProperty: 10};
     
    alert(a.testProperty); // 10, 点
    alert(a['testProperty']); // 10, 索引
     
    var propertyName = 'Property';
    alert(a['test' + propertyName]); // 10, 动态属性通过索引的方式

    这里有一个非常重要的特性——属性访问器总是使用ToObject规范来对待“.”左边的值。这种隐式转化和这句“在JavaScript中一切都是对象”有关系,(然而,当我们已经知道了,JavaScript里不是所有的值都是对象)。

    如果对原始值进行属性访问器取值,访问之前会先对原始值进行对象包装(包括原始值),然后通过包装的对象进行访问属性,属性访问以后,包装对象就会被删除。

    例如:

    var a = 10; // 原始值
     
    // 但是可以访问方法(就像对象一样)
    alert(a.toString()); // "10"
     
    // 此外,我们可以在a上创建一个心属性
    a.test = 100; // 好像是没问题的
     
    // 但,[[Get]]方法没有返回该属性的值,返回的却是undefined
    alert(a.test); // undefined

    那么,为什么整个例子里的原始值可以访问toString方法,而不能访问新创建的test属性呢?

    答案很简单:

    首先,正如我们所说,使用属性访问器以后,它已经不是原始值了,而是一个包装过的中间对象(整个例子是使用new Number(a)),而toString方法这时候是通过原型链查找到的:

    // 执行a.toString()的原理:
     
    1. wrapper = new Number(a);
    2. wrapper.toString(); // "10"
    3. delete wrapper;

    接下来,[[Put]]方法创建新属性时候,也是通过包装装的对象进行的:

    // 执行a.test = 100的原理:
     
    1. wrapper = new Number(a);
    2. wrapper.test = 100;
    3. delete wrapper;

    我们看到,在第3步的时候,包装的对象以及删除了,随着新创建的属性页被删除了——删除包装对象本身。

    然后使用[[Get]]获取test值的时候,再一次创建了包装对象,但这时候包装的对象已经没有test属性了,所以返回的是undefined:

    // 执行a.test的原理:
     
    1. wrapper = new Number(a);
    2. wrapper.test; // undefined

    这种方式解释了原始值的读取方式,另外,任何原始值如果经常用在访问属性的话,时间效率考虑,都是直接用一个对象替代它;与此相反,如果不经常访问,或者只是用于计算的话,到可以保留这种形式。

    继承

    我们知道,ECMAScript是使用基于原型的委托式继承。链和原型在原型链里已经提到过了。其实,所有委托的实现和原型链的查找分析都浓缩到[[Get]]方法了。

    如果你完全理解[[Get]]方法,那JavaScript中的继承这个问题将不解自答了。

    经常在论坛上谈论JavaScript中的继承时,我都是用一行代码来展示,事实上,我们不需要创建任何对象或函数,因为该语言已经是基于继承的了,代码如下:

    alert(1..toString()); // "1"

    我们已经知道了[[Get]]方法和属性访问器的原理了,我们来看看都发生了什么:

    1. 首先,从原始值1,通过new Number(1)创建包装对象
    2. 然后toString方法是从这个包装对象上继承得到的

    为什么是继承的? 因为在ECMAScript中的对象可以有自己的属性,包装对象在这种情况下没有toString方法。 因此它是从原理里继承的,即Number.prototype。

    注意有个微妙的地方,在上面的例子中的两个点不是一个错误。第一点是代表小数部分,第二个才是一个属性访问器:

    1.toString(); // 语法错误!
     
    (1).toString(); // OK
     
    1..toString(); // OK
     
    1['toString'](); // OK

    原型链

    让我们展示如何为用户定义对象创建原型链,非常简单:

    function A() {
      alert('A.[[Call]] activated');
      this.x = 10;
    }
    A.prototype.y = 20;
     
    var a = new A();
    alert([a.x, a.y]); // 10 (自身), 20 (继承)
     
    function B() {}
     
    // 最近的原型链方式就是设置对象的原型为另外一个新对象
    B.prototype = new A();
     
    // 修复原型的constructor属性,否则的话是A了 
    B.prototype.constructor = B;
     
    var b = new B();
    alert([b.x, b.y]); // 10, 20, 2个都是继承的
     
    // [[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属性,与基于class的继承相比,所有的属性都复制到后代子类里了。

    尽管如此,如果有需要(模拟基于类的继承)将x属性赋给B构造函数创建的对象上,有一些方法,我们后来来展示其中一种方式。

    其次,这不是一个特征而是缺点——子类原型创建的时候,构造函数的代码也执行了,我们可以看到消息"A.[[Call]] activated"显示了两次——当用A构造函数创建对象赋给B.prototype属性的时候,另外一场是a对象创建自身的时候!

    下面的例子比较关键,在父类的构造函数抛出的异常:可能实际对象创建的时候需要检查吧,但很明显,同样的case,也就是就是使用这些父对象作为原型的时候就会出错。

    function A(param) {
      if (!param) {
        throw 'Param required';
      }
      this.param = param;
    }
    A.prototype.x = 10;
     
    var a = new A(20);
    alert([a.x, a.param]); // 10, 20
     
    function B() {}
    B.prototype = new A(); // Error

    此外,在父类的构造函数有太多代码的话也是一种缺点。

    解决这些“功能”和问题,程序员使用原型链的标准模式(下面展示),主要目的就是在中间包装构造函数的创建,这些包装构造函数的链里包含需要的原型。

    function A() {
      alert('A.[[Call]] activated');
      this.x = 10;
    }
    A.prototype.y = 20;
     
    var a = new A();
    alert([a.x, a.y]); // 10 (自身), 20 (集成)
     
    function B() {
      // 或者使用A.apply(this, arguments)
      B.superproto.constructor.apply(this, arguments);
    }
     
    // 继承:通过空的中间构造函数将原型连在一起
    var F = function () {};
    F.prototype = A.prototype; // 引用
    B.prototype = new F();
    B.superproto = A.prototype; // 显示引用到另外一个原型上, "sugar"
     
    // 修复原型的constructor属性,否则的就是A了
    B.prototype.constructor = B;
     
    var b = new B();
    alert([b.x, b.y]); // 10 (自身), 20 (集成)

    注意,我们在b实例上创建了自己的x属性,通过B.superproto.constructor调用父构造函数来引用新创建对象的上下文。

    我们也修复了父构造函数在创建子原型的时候不需要的调用,此时,消息"A.[[Call]] activated"在需要的时候才会显示。

    为了在原型链里重复相同的行为(中间构造函数创建,设置superproto,恢复原始构造函数),下面的模板可以封装成一个非常方面的工具函数,其目的是连接原型的时候不是根据构造函数的实际名称。

    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); // 连接原型
     
    var b = new B();
    alert(b.x); // 10, 在A.prototype查找到

    也有很多语法形式(包装而成),但所有的语法行都是为了减少上述代码里的行为。

    例如,如果我们把中间的构造函数放到外面,就可以优化前面的代码(因此,只有一个函数被创建),然后重用它:

    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]]:

    function A() {}
    A.prototype.x = 10;
     
    function B() {}
    inherit(B, A);
     
    B.prototype.y = 20;
     
    B.prototype.foo = function () {
      alert("B#foo");
    };
     
    var b = new B();
    alert(b.x); // 10, 在A.prototype里查到
     
    function C() {}
    inherit(C, B);
     
    // 使用"superproto"语法糖
    // 调用父原型的同名方法
     
    C.ptototype.foo = function () {
      C.superproto.foo.call(this);
      alert("C#foo");
    };
     
    var c = new C();
    alert([c.x, c.y]); // 10, 20
     
    c.foo(); // B#foo, C#foo

    此文章大部分内容来自汤姆大叔的深入理解Javascript系列,链接:http://www.cnblogs.com/TomXu/archive/2011/12/15/2288411.html,本文在原文的基础上做了一些勘正,剔除了一些冗余的内容。感谢原文作者!

  • 相关阅读:
    Windows Phone7官方更新 增加复制粘贴
    使Apache(Linux)支持Silverlight
    WPF触摸屏项目案例(放置在奔驰公司触摸屏展厅)
    项目开发项目管理(转)
    诺基亚WP7手机界面曝光
    Windows 8十大传言:令苹果坐立不安
    如何为 iPad 打造速度超快的 HTML5 软件
    Silverlight5.0正式发布附下载地址
    我们的案例请访问我们团队官网http://Silverlighter.net
    【转】NTFS3G的安装和配置
  • 原文地址:https://www.cnblogs.com/inJS/p/4855153.html
Copyright © 2011-2022 走看看