JavaScript 混合对象“类”

混合对象“类”

混入

在继承或者实例化时,JavaScript 的对象机制并不会自动执行复制行为。简单来说,JavaScript 中只有对象,并不存在可以被实例化的“类”。一个对象并不会被复制到其他对象,它们会被关联起来。

【混入】:由于在其他语言中类表现出来的都是复制行为,因此 JavaScript 开发者也通过模拟类的复制行为创造出一个新的方法,名为“混入”。

显式混入

JavaScript 不会自动实现 Vehicle 到 Car 的复制行为,所以我们需要手动实现复制功能。

【示例】:

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!");
    }
});

【注意】:我们处理的已经不再是类了,因为在 JavaScript 中不存在类,Vehicle 和 Car 都是对象,供我们分别进行复制和粘贴。从技术角度来说,函数(ignition)实际上没有被复制,复制的是函数引用。

1. 再说多态

上例中 Vehicle.drive.call(this) 这就是我们所说的显式多态。在 ES6 之前的 JavaScript 没有相对多态的机制。所以,由于 Car 和 Vehicle 中都有 drive() 函数,为了指明调用对象,必须使用绝对(而不是相对)引用,即通过名称显式指定 Vehicle 对象并调用它的 drive() 函数。

【区别】:

  1. 在支持相对多态的面向类的语言中,Car 和 Vehicle 之间的联系只在类定义的开头被创建,从而只需要在这一个地方维护两个类的联系。
  2. 在 JavaScript 中由于屏蔽,使用显式伪多态会在所有需要使用(伪)多态引用的地方创建一个函数关联(我的理解是函数在 JavaScript 中是一等公民,而类在面向类的语言中是一等公民),这会极大地增加维护成本。由于显式伪多态可以模拟多重继承,所以它会进一步增加代码的复杂度和维护难度。

【建议】:使用伪多态通常会导致代码变得更加复杂,难以阅读并且难以维护,因此应当尽量避免使用显式伪多态,因为这样做往往得不偿失。

【示例】:显式伪多态模拟多重继承。

var Vehicle = {
    engine: 1,
    name: function() {
        console.log("I am vehicle!");
    }
};
var Car = {
    wheels: 4,
    name: function() {
        console.log("I am car!");
    }
};
var BaoMa = {
    name: function() {
        Vehicle.name.call(this);
        Car.name.call(this);
        console.log("I am BaoMa!");
    }
};
BaoMa.name();
// I am vehicle!
// I am car!
// I am BaoMa!
2. 混合复制

【mixin() 工作原理】:遍历 sourceObj(父类)的属性,如果在 targetObj(子类)中没有这个属性就进行复制。由于实在目标对象(子类)初始化之后才进行复制,因此要小心不要覆盖目标对象的原有属性。

【注意】:如果没有存在性检查,可能会有重写风险。

【另外一种方法】:先进行复制后对子类进行特殊化,这么做的好处在于不需要进行存在性检查。

function mixin(sourceObj, targetObj) {
    for(var key in sourceObj) {
        targetObj[key] = sourceObj[key];
    }
    
    return targetObj;
}

var Vehicle = {
    // ...
};

// 首先创建一个空对象并把 Vehicle 的内容复制进去
var Car = mixin(Vehicle, {});

// 然后把新内容复制到 Car 中
mixin({
    wheels: 4,
    drive: function() {
        // ...
    }
}, Car);

【问题】:由于两个对象引用的是同一个函数,因此这种复制(或者说混入)实际上并不能完全模拟面向类的语言中的复制。也就是说 JavaScript 中的函数无法真正地复制,所以你只能复制对共享函数对象的引用。如果你修改了共享的函数对象,其所有引用该函数的对象都会受到影响。

【说明】:显式混入是 JavaScript 中一个很棒的机制,不过它的功能也没有看起来那么强大。虽然它可以把一个对象的属性复制到另一个对象中,但是这其实并不能带来太多的好处,无非就是少几条定义语句,而且还会带来我们刚才提到的函数对象引用问题。

【注意】:在能够提高代码可读性的前提下使用显式混入,避免使用增加代码理解难度或者让对象关系更加复杂的模式。

3. 寄生继承

显式混入模式的一种变体被称为“寄生继承”,它既是显式的又是隐式的,主要推广者是 Douglas Crockford。

【工作原理】:

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!

【解释】:首先复制一份 Vehicle 父类(对象)的定义,然后混入子类(对象)的定义(如果需要的话保留到父类的特殊引用),然后用这个复合对象构建实例。

【注意】:调用 new Car() 时会创建一个新对象并绑定到 Car 的 this 上。但是因为我们没有使用这个对象而是返回了我们自己的 car 对象,所以最初被创建的这个对象会被丢弃,因此可以不使用 new 关键字直接调用 Car()。这样做得到的结果是一样的,但是可以避免创建并丢弃多余的对象。

隐式混入

隐式混入和之前提到的显式伪多态很像,因此也具备同样的问题。

【示例】:

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

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

【解释】:通过在构造函数调用或者方法调用中使用 Something.cool.call(this),实际上“借用”了函数 Something.cool() 并在 Another 的上下文中调用了它。最终的结果是 Something.cool() 中的赋值操作都会应用在 Another 对象上而不是 Something 对象上。

【注意】:虽然这类技术利用了 this 的重新绑定功能,但是 Something.cool.call(this) 仍然无法变成相对引用,所以使用时千万要小心。通常来说,尽量避免使用这样的结构,以保证代码的整洁和可维护性。

小结

  • JavaScript 不会像面向类语言那样自动创建对象的副本。
  • 混入模式(无论是显式还是隐式)可以用来模拟类的复制行为,但是通常会产生丑陋并且脆弱的语法,比如显式伪多态,这会让代码更加难懂并且难以维护。此外,显式混入实际上无法完全模拟类的复制行为,因为对象只能复制引用,无法复制被引用对象或者函数本身。忽视这一点会导致许多问题。
  • 总的来说,在 JavaScript 中模拟类是得不尝试的,虽然能解决当前的问题,但是可能会埋下更多的隐患。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容