你不知道的js系列上(45)-显式混入(代码片段)

Zina Zina     2022-12-25     166

关键词:

JS 的对象机制并不会自动执行复制行为,由于其他语言中表现出来的复制行为,因此 JS 开发者也想出了一个方式来模拟类的复制行为,这个方法就是混入。我们先看第一种,显式混入

// 非常简单的 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, 
  wheel: 4,
  drive: function()
    Vehicle.drive.call(this);
    console.log(\'Rolling on all \' + this.wheel + \' wheels!\');
  
)

我们处理的已经不再是类了,因为在 JS 中不存在类,Vehicle 和 Car 都是对象。现在 Car 中就有了一份 Vehicle 属性和函数的副本。从技术角度来说,函数实际上没有被复制,复制的是函数引用。Car 已经有了 drive 属性,所以这个属性引用没有被 mixin 重写,从而保留了 Car 中定义的同名属性,实现了 ‘子类’ 对 ‘父类’ 属性的重写。


我们再类分析一下这条语句 Vehicle.drive.call(this)。 这就是所说的显式多态。之前我们代码中 super.drive(),我们称之为相对多态。由于 Car 和 Vehicle 中都有 drive() 函数,为了指明调用对象。我们通过名称显示制定 Vehicle 对象并调用它的 drive() 函数。

但是如果直接执行 Vehicle.drive(),函数调用中的 this 会被绑定到 Vehicle 对象而不是 Car 对象,这并不是我们想要的。

我们分析一下 mixin 的工作原理。它会遍历 sourceObj 的属性,如果在 targetObj 没有这个属性就会进行复制。

如果我们是先进行复制然后对 Car 进行特殊化对话,就可以跳过存在性检查。不过这种方法并不好并且效率更低,所以不如第一种方法常用:
// 另一种混入函数,可能又重写风险
function mixin(sourceObj, targetObj) 
  for (var key in sourceObj) 
    targetObj[key] = sourceObj[key];
  
  return targetObj;


var Vehicle =   // ...
// 首先创建一个空对象并把 Vehicle 对内容复制进去 var Car = mixin( Vehicle, ); // 然后把新内容复制到 Car 中 mixin(   wheel: 4,   drive: function()     // ...    , Car)

这两种方法都可以把不重叠对内容从 Vehicle 中显示复制到 Car 中。‘混入’ 这个名字来源与这个过程对另一种解释: Car 中混合了 Vehicle 的内容,所以这叫混合复制。复制操作完成后, Car 和 Vehicle 分离了,向 Car 中添加属性不会影响到 Vehicle,反之亦然。


JS 中的函数无法真正的复制,所以只能复制对共享函数的引用,如果修改了共享函数,那么 Vehicle 和 Car 都会受到影响。

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

之前我们说只有父类和子类,这个比喻不太恰当,因为现实生活中,除了父类,还有母类。绝大多数后代是由双亲产生的。如果类可以继承两个类,那看起来更符合现实的比喻了。有些面向类的语言允许继承多个‘父类’。多重继承意味着所有父类的定义都会复制到子类中。

JS 中本身并不提供多重继承的功能,许多人认为这是件好事,因为使用多重继承的代价太高,然而这无法阻止开发者门的热情,他们会尝试多种方法来实现多重继承

比如此例中,如果向目标函数显示混入多个对象,就可以部分模仿多重继承行为,但是从根本上来说,使用这些‘诡计’通常会得不偿失


显示混入模式还有一种变体被称为 “寄生继承”
// 传统的 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();

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

 

你不知道的js系列(12)-声明提升(代码片段)

我们直觉上会认为JavaScript代码在执行时是由上到下一行一行执行的。但实际这并不完全正确 a=2;vara;console.log(a);这里可能会认为是undefined,因为vara声明在a=2之后。实际输出了2。 console.log(a);vara=2;鉴于上面的代码可能会是2... 查看详情

你不知道的js系列(14)-闭包无处不在(代码片段)

上一节的闭包是为了解释如何使用闭包而人为地在结构上进行修饰,在昨天的闭包基础上,我们可以更加灵活的使用闭包functionwait(message)  setTimeout(functiontimer()    console.log(message)  ,1000)wait(‘hello,consure‘);内部函数timer... 查看详情

你不知道的js系列(30)-对象属性(可计算属性名)(代码片段)

如果你需要通过表达式来计算属性名,[]操作符就派上用场了。ES6中使用[]包裹一个表达式来当作属性名varprefix=‘foo‘;varmyObject=  [prefix+‘bar‘]:‘hello‘,  [prefix+‘baz‘]:‘world‘,myObject[‘foobar‘];//hellomyObject[‘foobaz‘];//world... 查看详情

你不知道的js系列(33)-对象复制(代码片段)

JS初学者最常见的问题之一就是如何复制一个对象。看起来应该有一个内置的copy()方法,实际上比想象中的更复杂,我们无法选择一个默认的复制算法functionanotherFunction()/**...*/;varanotherObject=c:true;varanotherArray=[];varmyObject=  a:2, ... 查看详情

你不知道的js系列(39)-对象遍历(代码片段)

for循环可以遍历数组varmyArray=[1,2,3];for(vari=0;i<myArray.length;i++)  console.log(myArray[i])//123ES5增加了数组的辅助迭代器,包括forEach(...)、every(...)、some(...)forEach(...)会遍历数组中的所有值并忽略回调函数的返回值every(...)会一直运行直... 查看详情

你不知道的js系列(13)-什么是闭包(代码片段)

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行functionfoo()  vara=2;  functionbar()    console.log(a);    returnbarvarbaz=foo();baz();//2——朋友,这就是闭包的效果在foo... 查看详情

你不知道的js系列(29)-对象属性(代码片段)

存储在对象容器内部的是这些属性的名称,它们就像指针(从技术角度来说是引用)一样,指向这些值真正的存储位置。 varmyObject=  a:2myObject.a;//2myObject[‘a‘];//2.语法通常被称为‘属性访问’,[]语法通常被称为&lsqu... 查看详情

你不知道的js系列(22)-thisnew绑定(代码片段)

在传统的面向类的语言中,“构造函数“是类中的一些特殊方法,使用new初始化类时会调用类中的构造函数。通常的形式是这样的something=newMyClass(..);然而JavaScript中new的机制实际上和面向类的语言不同。它们只是被new操作... 查看详情

你不知道的js系列(7)-欺骗词法作用域(代码片段)

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”词法作用域呢?有些人喜欢特殊的办法来解决遇到的问题。我们规定词法作用域是代码写在哪里决定的,一旦决定了无法更改,因... 查看详情

你不知道的js系列-引擎怎么查找变量(代码片段)

对代码进行处理的三个角色引擎:从头到尾负责整个JavaScript程序的编译和执行过程编译器:负责语法分析及代码生成等作用域:负责收集并维护所有变量的查询 vara=2;编译器首先会将这段程序分解成词法单元,然后将词法单... 查看详情

你不知道的js系列(19)-this调用位置(代码片段)

我们排除了一些对于this对错误理解并且明白了每个函数的this是在调用时被绑定的,完全取决于函数的调用位置。寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏... 查看详情

你不知道的js系列(5)-词法作用域(代码片段)

作用域分为两种,一种是词法作用域,一种是动态作用域,我们先看第一种,词法作用域 词法作用域就是定义在词法阶段的作用域(编译器的第一个工作阶段叫做词法化,词法化的过程会对源代码中的字符进行检查)。换句... 查看详情

你不知道的js系列(23)-this绑定优先级(代码片段)

我们首先来看下隐式绑定和显示绑定哪个优先级更高functionfoo()  console.log(this.a)varobj1=  a:2,  foo:foovarobj2=  a:3,  foo:fooobj1.foo();//2obj2.foo();//3obj1.foo.call(obj2);//3obj2.foo.call(obj1);//2这个例子可以看到,显示绑定优先级比 查看详情

你不知道的sring(系列三)(代码片段)

AbstractStringBuilderStringBuffer和StringBuilder都继承了AbstractStringBuilder,很多方法都是直接super的父类AbstractStringBuilder的方法,所以我们分析下AbstractStringBuilder的源码.1.成员变量AbstractStringBuilder和String一样,在其内部都是以字符数组的形... 查看详情

《你不知道的js(中卷)》混合对象“类”(代码片段)

四、混合对象“类”:? 在研究类的具体机制之前,首先介绍面向类的设计模式:实例化(instantiation)、继承(inheritance)和(相对)多态(polymorphism)。一)、类理论:? 面向对象编程强调的是数据和操作数据的行为本质上是... 查看详情

《你不知道的js(中卷)》关于this(代码片段)

一、关于this:一)、为什么要用this?functionidentity() returnthis.name.toUpperCase();varme= name:"Kyle";varyou= name:"Reader";identity.call(me);//Kyleidentity.call(you);//Reader? 观察上面的代码,使 查看详情

你不知道的javascript系列中(12)-特殊数值无穷数(代码片段)

熟悉传统语言(C)的开发人员可能都遇到过编译错误(compilererror)或者运行时错误(runtimeexception),例如“除以0”。然后在JavaScript中上例的结果为Infinity vara=1/0;//Infinityvara=-1/0;//-Infinity如果除法运算中的一个操作数... 查看详情

你不知道的javascript系列中(12)-特殊数值无穷数(代码片段)

熟悉传统语言(C)的开发人员可能都遇到过编译错误(compilererror)或者运行时错误(runtimeexception),例如“除以0”。然后在JavaScript中上例的结果为Infinity vara=1/0;//Infinityvara=-1/0;//-Infinity如果除法运算中的一个操作数... 查看详情