美文网首页
继承或者委托

继承或者委托

作者: 樱木夜访流川枫 | 来源:发表于2018-07-29 19:16 被阅读0次

一 概览

  这篇文章是我通过阅读《JavaScript 高级程序设计》和《你不知道的JavaScript》中关于 继承 模块的一点心得。

二 面向对象回顾

面向类编程

  你是否还记得大学里面刚学C++的时候关于面向对象的介绍呢,让我们一块来回顾一下吧。
  类的 定义:在面向对象编程中,类是一种 代码组织结构形式,一种从真实世界到软件设计的建模方法。
  类的 组织形式:面向对象或者面向类编程强调 数据操作数据的行为 应该 封装 在一起,在正式计算机科学中我们称为 数据结构

类与23种高级设计模式

  类是面向对象的 底层设计模式,它是面向对象23种高级设计模式的 底层机制
  你或许还听说过 过程化编程,一种不借助高级抽象,仅仅由 过程(函数)调用 来组织代码的编程方式。程序语言中,Java只支持面向类编程,C/C++/Php既支持过程化编程,也支持面向类编程。

类的机制

  在类的设计模式中,它为我们提供了 实例化继承多态 3种机制。
  构造器:类的实例由类的一种特殊方法构建,这个方法的名称通常与类名相同,称为 “构造器(constructor)”。这个方法的明确的工作,就是初始化实例所需的所有信息(状态)。
  实例化:借助构造函数,由通用类到具体对象的过程。
  继承:子类通过 拷贝(请一定要记住这个词)父类的属性和方法,从而使自己也能拥有这些属性与方法的过程。
  多态:由继承产生的,子类重写从父类中继承的属性和方法,从而子类更加具体。

类的继承

(1)相对多态:任何方法都可以引用位于继承层级上更高一层的其他方法(同名或不同名)。我们说“相对”,因为我们不绝对定义我们想访问继承的哪一层(也就是类),而实质上在说“向上一层”来相对地引用。
(2)超类:在许多语言中,使用 super 关键字来引用 父类或祖先类
(3)如果子类覆盖父类的某个方法,原版的方法和覆盖后的方法都是可以存在的,允许访问。
(4) 不要让多态搞糊涂,子类并不是链接到父类上,子类只是父类的一个副本,类继承的实质是拷贝行为
(5)多重继承:子类的父类不止一个,JavaScript不支持多重继承

混入

原理: 子构造函数混入父构造函数的属性和方法。
JavaScript的复合类型以 引用 的方式传递,不支持拷贝行为。混入(Mixin)手动拷贝 的方式模拟继承的拷贝行为。

明确混入:

(1)定义:显示的把一个对象的属性混入另一个对象。
(2)实现如下:

// 另一种mixin,对覆盖不太“安全”
// 大幅简化的`mixin(..)`示例:
function mixin( sourceObj, targetObj ) {
    for (var key in sourceObj) {
        // 仅拷贝非既存内容
        if (!(key in targetObj)) {
            targetObj[key] = sourceObj[key];
        }
    }

    return targetObj;
}

var Vehicle = {
    engines: 1,

    ignition: function() {
        console.log( "Turning on my engine." );
    },

    drive: function() {
        this.ignition();
        console.log( "Steering and moving forward!" );
    }
};

var Car = mixin( Vehicle, {
    wheels: 4,

    drive: function() {
        Vehicle.drive.call( this );
        console.log( "Rolling on all " + this.wheels + " wheels!" );
    }
} );

(3)显示假想多态:Vehicle.drive.call(this)。因为ES6之前,JavaScript无法实现相对多态(inherit:drive()),所以我们明确地用名称指出Vehicle对象,然后在它上面调用drive()函数。
(4)问题
  A.技术上讲,函数没有被复制,只是复制了函数的引用;
  B.在每一个需要建立 假想多态 引用的函数中都需要建立手动链接(Vehicle.drive.call(this)),维护成本高。可以尝试通过它实现 多重继承
(5)结论:明确混入复杂、难懂、维护成本高,不推荐使用。

寄生继承:

(1)明确的mixin模式的一个变种,在某种意义上是明确的而在某种意义上是隐含的。
(2)实现如下:在子构造函数中new一个如果找函数的实例对象,在这个对象上扩展属性、方法,最后将这个对象返回。

// “传统的JS类” `Vehicle`
function Vehicle() {
    this.engines = 1;
}
Vehicle.prototype.ignition = function() {
    console.log( "Turning on my engine." );
};
Vehicle.prototype.drive = function() {
    this.ignition();
    console.log( "Steering and moving forward!" );
};

// “寄生类” `Car`
function Car() {
    // 首先, `car`是一个`Vehicle`
    var car = new Vehicle();

    // 现在, 我们修改`car`使它特化
    car.wheels = 4;

    // 保存一个`Vehicle::drive()`的引用
    var vehDrive = car.drive;

    // 覆盖 `Vehicle::drive()`
    car.drive = function() {
        vehDrive.call( this );
        console.log( "Rolling on all " + this.wheels + " wheels!" );
    };
    return car;
}

var myCar = new Car();

myCar.drive();
// Turning on my engine.
// Steering and moving forward!
// Rolling on all 4 wheels!

(3)问题:子函数的初始化创建对象丢失,改变了this绑定,不过不用new去直接创建。

隐式混入

(1)定义:父、子构造函数在原有构造函数与属性、方法之间,添加一层函数,子构造函数中间函数的this绑定到父构造函数中间函数
(2)实现原理:利用了this的二次绑定。
(3) 实现如下:

var Something = {
    cool: function() {
        this.greeting = "Hello World";
        this.count = this.count ? this.count + 1 : 1;
    }
};

Something.cool();
Something.greeting; // "Hello World"
Something.count; // 1

var Another = {
    cool: function() {
        // 隐式地将`Something`混入`Another`
        Something.cool.call( this );
    }
};

Another.cool();
Another.greeting; // "Hello World"
Another.count; // 1 (不会和`Something`共享状态)

(4) 问题:单纯的利用this的二次绑定,不能实现相对应用。
(5) 结论:谨慎使用。

三 原型

prototype

prototype 定义:JavaScript中每个对象都拥有一个prototype属性,它只是一个 其他对象的引用。几乎所有的对象在被创建时,它的这个属性都被赋予了一个 非null 值。

“类”函数

代码如下:

function Foo() {
    // ...
}

var a = new Foo();

Object.getPrototypeOf( a ) === Foo.prototype; // true

结论: 当通过调用new Foo()创建实例对象时,实例对象会被链接到Foo.prototype指向的对象。

拷贝与链接

代码如下:

function Foo() {

}

Foo.prototype.fruit = ['apple'];

// foo1的[prototype]链接到了 Foo.prototype
var foo1 = new Foo();
foo1.fruit.push('banana');

// foo2的[prototype]也被链接到了 Foo.prototype
var foo2 = new Foo();
foo2.fruit // [apple, banana]

  在面向类的语言中,可以创造一个类的多个拷贝。在JavaScript中,我们不能创造一个类的多个实例,可以创建多个对象,它们的[prototype]链接指向一个共同对象。但默认地,没有拷贝发生,如此这些对象彼此间最终不会完全分离和切断关系,而是 链接在一起。

  “继承”意味着 拷贝 操作,而JavaScript不拷贝对象属性(原生上,默认地)。相反,JS在两个对象间建立链接,一个对象实质上可以将对属性/函数的访问 委托 到另一个对象上。对于描述JavaScript对象链接机制来说,“委托”是一个准确得多的术语。

new调用和普通调用本质相同

  JavaScript中,new在某种意义上劫持了普通函数,并将它以另一种函数调用:构建一个对象,外加调用这个函数所做的任何事。

实例对象没有constructor属性
function Foo() { }

var foo1 = new Foo();

foo1.constructor === Foo  // true

// 修改Foo.prototype指向的对象
Foo.prototype = {
    //
}

var foo2 = new Foo();

foo2.constructor === Foo  // false

  a.constructor === Foo为true意味着a上实际拥有一个.constructor属性,指向Foo?不对
  实际上,.constructor引用也 委托 到了Foo.prototype,它 恰好 有一个指向Foo的默认属性。

3 “原型继承”

原型继承分析

代码如下:


function Foo(name) {
    this.name = name;
}

Foo.prototype.myName = function() {
    return this.name;
};

function Bar(name,label) {
    // 构造函数内部相对多态
    Foo.call( this, name );
    this.label = label;
}

// 这里,我们创建一个新的`Bar.prototype`链接链到`Foo.prototype`
Bar.prototype = Object.create( Foo.prototype );

// 注意!现在`Bar.prototype.constructor`不存在了,
// 如果你有依赖这个属性的习惯的话,可以被手动“修复”。
Bar.prototype.myLabel = function() {
    return this.label;
};

var a = new Bar( "a", "obj a" );

a.myName(); // "a"
a.myLabel(); // "obj a"

核心代码分析
代码1:


function Bar(para1, para2) {
   Foo.call(this, para1);
   //...
}

代码1分析:构造函数内部初始化,利用this绑定,根据父构造函数初始化子构造函数内部。

代码2:

Bar.prototype = Object.create(Foo.prototype)

代码2分析:原型初始化,将子构造函数的[prototype]链接到父构造函数的[prototype]链接的对象。

误区

Bar.prototype = Foo.prototype

这种方法是错误的,子构造函数会污染到父构造函数

ES6 新方法

Object.setPrototypeOf(Bar.prototype, Foo.prototype)
“自身”

  面向类语言中,根据实例对象查找创建它的类模板,称为自省(或反射)。JavaScript中,如何根据实例对象,查找它的委托链接呢?

1 instanceOf:

代码如下:

function Foo() {
    //...
}

var a = new Foo();

a instanceOf Foo  // true

代码分析:
a: instanceOf 机制,在实例对象(a)的原型链中,是否有Foo.prototype;
b: 需要用于可检测的构造函数(Foo);
c: 无法判断实例对象间(比如a,b)是否通过[prototype]链相互关联。

2 isPrototypeOf [[prototype]]反射

代码如下:

function Foo() {
    // ...
}

var a = new Foo();

Foo.prototype.isPrototypeOf(a);  // true

// 对象b是否在a的[[prototype]]链出现过
b.isPrototypeOf(a);

代码分析
a:在实例对象(a)的原型链中,是否有Foo.prototype;
b:需要用于可检测的构造函数(Foo);
c:可以判断对象间是否通过[prototype]链相互关联。

3 getPrototypeOf 获取原型链

代码如下:

function Foo() {
    // ...
}

var a = new Foo();

Object.getPrototypeOf(a)  // 查看constructor属性

4 proto

代码如下:

function Foo() {
    // ...
}

var a = new Foo();

a.__proto__ === Foo.prototype  // true

代码分析:
a:proto属性在ES6被标准化;
b:proto属性跟 constructor属性类似,它不存在实例对象中。constructor属性存在于 原型链中,proto存在于Object.prototype中。
c:proto看起来像一个属性,但实际上将它看做是一个getter/setter更合适。

Object.defineProperty( Object.prototype, "__proto__", {
    get: function() {
        return Object.getPrototypeOf( this );
    },
    set: function(o) {
        // setPrototypeOf(..) as of ES6
        Object.setPrototypeOf( this, o );
        return o;
    }
} );

对象关联

创建关联

代码如下:

var foo = {
    printFoo: function() {
        console.log('foo');
    }
}

var a = Object.create(foo);

a.printFoo();  // 'foo'

代码分析:
a、Object.create()会创建一个对象(a),并把它链接到指定对象(foo);
b、相比new 调用,Object.create()不会产生 prototype引用constructor引用

关联是否备用

代码如下:

var anotherObject = {
    cool: function() {
        console.log('cool');
    }
}

var bar = Object.create(anotherObject);

bar.cool();  // 'cool'

代码分析:
a、单纯的在bar无法处理属性或方法时,建立备用链接(anotherObject),代码会变得很难理解和维护,这种模式应该慎重使用;
b、ES6提供“代理”功能,它实现的就是“方法无法找到”时的行为。

相关文章

网友评论

      本文标题:继承或者委托

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