美文网首页
《JavaScript 核心》(1):对象、原型和继承

《JavaScript 核心》(1):对象、原型和继承

作者: gaoyanglol | 来源:发表于2019-09-29 13:14 被阅读0次

    本文译自:JavaScript. The Core. - Dmitry Soshnikov

    对象

    ECMAScript 是一门高度抽象化的面向对象语言,主要和对象打交道。虽然也有原始值,但是当需要的时候也会被转换为对象。

    对象是一个由属性组成的集合,且有单一的原型。它的原型要么是一个对象,要么是 null

    我们来看一个简单的对象示例。一个对象的原型由内部的 [[Prototype]] 属性引用。但是在用户级的代码中,我们用 __proto__ 来实现该引用,可以读作 'dunder proto' 。

    var foo = {
        x: 10,
        y: 20
    }
    

    我们会得到这样一种结构,它有两个显式的自有属性 xy。还有一个隐式的 __proto__ 属性,指向 foo 的原型。

    图1. 一个指向原型的对象

    原型有什么用?我们用原型链的概念来回答这个问题。

    原型链

    原型其实就是带有自有属性的对象。原型A指向它自身的原型——原型B,原型B再指向自身的原型——原型C,直到最终指向的原型为 null。这就称为原型链

    原型链是一个由对象组成的有限链,用来实现继承共享属性

    假设我们有两个对象,它们只有很小一部分有区别,其余的部分都一样。显然,一个设计良好的系统会重用相似的功能/代码,而不是在每一个对象中重复一遍。在基于类的语言中,这种代码重用的语式称为基于类的继承——把相似的功能放入类 A ,再创建出继承自 A 且拥有自身额外小改动的 BC

    ECMAScript 没有类的概念。不过代码重用的语式没有太大区别(在有些方面甚至比类更加灵活),通过原型链就可以实现。这种继承方式称作委托继承(或者更接近 ECMAScript 的说法是,原型继承)。

    和类 ABC 的例子相似,在 ECMAScript 中,你会创建对象 a, b, c。对象 a 中存放对象 bc 的相同部分,bc 中只存放它们自身的额外属性或方法。

    var a = {
        x: 10,
        calculate: function (z) {
            return this.x + this.y + z
        }
    }
    
    var b = {
        y: 20,
        __proto__: a
    }
    
    var c = {
        y: 30,
        __proto__: a
    }
    
    // 调用继承方法
    b.calculate(30) // 60
    c.calculate(40) // 80
    

    很简单对吧?我们可以看到 bc 都能访问对象 a 中定义的 calculate 方法。这正是通过原型链来实现的。

    原理很简单:如果一个属性或方法在对象自身中无法找到(比如对象没有自有属性),那么就尝试在原型链中寻找该属性/方法。如果在对象的原型中也找不到该属性,那么就在原型的原型中找,如此往复,直到遍历整个原型链(与基于类的继承做法完全一样,当解析一个继承方法时——我们也会找遍类型链)。第一个找到的同名属性/方法将被引用。找到的这个属性称作继承属性。如果在整个原型链中都找不到这个属性,则返回 undefined

    注意,在调用继承方法时,其中的 this 绑定的是调用该方法的原始对象而不是该方法所在的原型对象。在上面的示例中 this.y 的值取自对象 bc ,而不是 a 。不过 this.x 的值取自 a ,同样是通过原型链机制。

    如果一个对象没有明确的指定其原型,则其 __proto__ 默认指向原型 Object.prototype

    原型 Object.prototype 自身也有 __proto__ 属性,它指向原型链的最后一环 null

    下图展示了对象 abc 的继承结构。

    图2. 原型链

    注意:

    • ES5中制定了另外一种原型继承的方法,使用 Object.create 函数:

      var b = Object.create(a, {y: {value: 20}})
      var c = Object.create(a, {y: {value: 30}})
      
    • 你可以在这一章中获取更多关于 ES5 API 的信息。

    • ES6 已经将 __proto__ 纳入标准,它可以用于对象的初始化。

    我们经常会需要用到一些有相同或相似声明结构(比如相同属性)但声明值不同的对象。这种情况我们可以使用构造函数,它能用特定的格式创建对象。

    构造函数

    除了用特定格式创建对象,构造函数还有一个重要的作用 —— 它会为新创建的对象自动指定一个原型。这个原型就存放在 ConstructorFunction.prototype 属性里。

    我们可以用构造函数重写前面例子中的对象 bc 。这样,Foo.prototype 就扮演了对象 a 的角色:

    // 一个构造函数
    function Foo(y) {
        // 可以用固定格式创建对象:
        // 他们有后生成的自有 'y' 属性
        this.y = y
    }
    // 同时 "Foo.prototype" 里存放着新创建对象的原型的引用,
    // 所以我们可以用它来定义共享的/继承的属性或方法,于是和前面例子一样,我们创建:
    
    // 继承属性 "x"
    Foo.prototype.x = 10
    
    // 还有继承方法 "calculate"
    Foo.prototype.calculate = function (z) {
        return this.x + this.y + z
    }
    
    // 再来用“模板” Foo 创建对象 "b" 和 "c"
    var b = new Foo(20)
    var c = new Foo(30)
    
    // 调用继承方法
    b.calculate(30) // 60
    c.calculate(40) // 80
    
    // 来看看属性引用是否和预期的一样
    console.log(
        b.__proto__ === Foo.prototype, // true
        c.__proto__ === Foo.prototype, // true
    
        // 同时 "Foo.prototype" 自动创建一个特殊属性 "constructor" ,
        // 指向构造函数本身;
        // 实例对象 "b" 和 "c" 可以透过委托找到该属性并且用它来查看它们的构造器。
    
        b.constructor === Foo, // true
        c.constructor === Foo, // true
        Foo.prototype.constructor === Foo, // true
    
        b.calculate === b.__proto__.calculate, // true
        b.__proto__.calculate === Foo.prototype.calculate // true
    )
    

    这段代码可以用下图的关系来表达:

    图3. 构造函数和对象间的关系

    这张图再次展示了每一个对象都有原型。构造函数 Foo 也有自己的 __proto__ ,它指向 Function.prototype ,而 Function.prototype 又通过 __proto__ 指向 Object.prototype 。`

    Foo.prototype 就是 Foo 的一个显式属性。它是对象 bc 的原型。

    严格来说,如果要分类的话,构造函数和原型的组合可以称作“类”。实际上,像 Python 的头等动态类的实现和属性/方法这种解决方案完全一样。由此看来,Python 中的类其实是 ECMAScript 委托继承的一种语法糖。

    注意:

    • 在 ES6 中,类 “class” 的概念已经纳入标准,作为上面所述的构造函数的语法糖。由此看来,原型链成为了类继承的一种详细实现。
    // 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
    

    在 ES3 系列文章的第7章中可以找到这部分内容更完整和详细的解析。其中分为两个部分:7.1.面向对象编程:概论,在这部分中你可以找到各种面向对象编程的范例和语式,以及它们和 ECMAScript 的对比,还有 7.2.面向对象编程:ECMAScript 实现,完全忠于 ECMAScript 中的面向对象编程实现。

    现在我们已经了解了对象的基本面,继续来看运行时程序执行在 ECMAScript 中如何实现。这就是所谓的一个执行上下文栈,其中的每一个元素都可以抽象地用对象来代表。没错,ECMAScript 中几乎所有地方都用对象的概念运作。

    相关文章

      网友评论

          本文标题:《JavaScript 核心》(1):对象、原型和继承

          本文链接:https://www.haomeiwen.com/subject/xeahuctx.html