继承或者委托

一 概览

  这篇文章是我通过阅读《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提供“代理”功能,它实现的就是“方法无法找到”时的行为。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容