关于js中原生构造函数的继承

前言

最近参加了蚂蚁金服的面试,一个关于js继承的问题答的不是特别好。在如今快节奏的工作当中,很多基础的东西会渐渐被丢掉。就如继承这个话题,写React的同学应该都是class xxx extends React.Component,然而这可以理解为es5的一个语法糖,所以问题又回到了js如何实现继承。面试结束后,赶紧翻了翻积满灰尘的js高级程序设计,重新学习了一遍面向对象这一章,有一个创建对象的模式吸引到了我。

寄生构造函数模式

在oo中我们是通过类去创建自定义类型的对象,然而js中没有类的概念,在es5的时代,如果我们要去模拟类,学过的同学应该知道最好采用一种构造函数与原型混成的模式。而书中作者提到了一种有意思的模式,叫做寄生构造函数模式,代码如下:

function Person(name, age, job) {
    var o = new Object();
    o.name = name;
    o.age = age;
    o.job = job;
    o.sayName = function() {
        alert(this.name);
    };
    return o;
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName(); // "Nicholas"

对于这种模式有诸多不解:

  1. 仔细一看,这特么不就是所谓的工厂函数模式吗?工厂模式的几个缺点它都存在,一种是创建的所有对象均为Object类型,无法进行类型识别;其次每次创建对象都会重新生成一个function用来创建sayName属性,浪费内存。
  2. 这里的new有什么意义吗?new的作用是生成一个对象,将当前上下文即this指向该对象,然后return该对象。但是此处return了一个o,new就完全没用了。

    带着诸多的不解,又看到了作者提到了该模式的一个使用场景,看代码:

function SpecialArray() {
    // 创建数组
    var values = new Array();
    // 添加值
    values.push.apply(values, arguments);
    // 添加方法
    values.toPipedString = function() {
        return this.join("|");
    };
    // 返回数组
    return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); // "red|blue|green"

从代码我们得知,该构造函数是希望创建一个具有额外方法的特殊数组,仔细想想,这不就是继承嘛。继承在书中提到的最棒的方式是通过寄生组合式继承,那为什么还要通过这种方式来实现Array继承,况且该方式有个很大的问题就是上面提到的类型无法通过instanceof来确定。

寄生组合式继承

我们先来看看最常用的继承范式:寄生组合式继承,写法如下:

function SpecialArray() {
  // 调用Array函数,绑定给当前上下文
  Array.apply(this, arguments);
};

// 创建一个以Array.prototype为原型的对象作为SpecialArray的原型
SpecialArray.prototype = Object.create(Array.prototype);

// constructor指向SpecialArray,默认情况[[enumerable]]为false
Object.defineProperty(SpecialArray.prototype, "constructor", {
  enumerable: false,
  value: SpecialArray
});

SpecialArray.prototype.toPipedString = function() {
  return this.join("|");
};

var arr = new SpecialArray(1, 2, 3);

console.log(arr); // arr为SpecialArray {}
console.log(new Array(1, 2, 3).hasOwnProperty(‘length‘)) // true 证明length是Array的实例属性
console.log(arr.hasOwnProperty(‘length‘)) // false 证明Array无视apply方法的this绑定

上面是典型的寄生组合式继承的写法,其存在几个问题:

  1. new的行为上面介绍过,它会返回对象类型,而我们的SpecialArray希望像Array一样,new的时候返回数组。
  2. 我们先通过hasOwnProperty证明了length是Array的一个实例属性,既然如此通过执行Array.apply(this, arguments)会将length绑定给SpecialArray的实例arr,但是实际arr上没有length属性,因此可以证明Array无视apply方法的this绑定。

既然this无法绑定,那我们只能通过new一个Array来帮我们构造一个数组实例并返回,此时我们的构造函数应该像这样:

function SpecialArray() {
  var values = new Array()
  // 添加初始值
  values.push.apply(values, arguments);
  return values
};

这其实就是我们上面提到的寄生构造函数模式,但是此时返回的values是Array的实例,其原型对象是Array.prototype。这样会造成两个问题:

  1. 无法通过instanceof确定实例的类型,它始终为Array的实例
  2. 我们希望将构造函数的方法放入prototype实现共享,而不是放入构造函数中,在每次生成实例都重新生成一个function

因此我们要做的事情就是将生成的values实例的原型指向SpecialArray.prototype。我们知道实例对象有一个__proto__属性,它指向其构造函数的原型,我们可以通过修改该属性达到我们的目的:

function SpecialArray() {
  var values = new Array()
  // 添加初始值
  values.push.apply(values, arguments);
  // 将values的原型指向SpecialArray.prototype
  values.__proto__ = SpecialArray.prototype
  return values
};

// 创建一个以Array.prototype为原型的对象作为SpecialArray的原型
SpecialArray.prototype = Object.create(Array.prototype);

// constructor指向SpecialArray,默认情况[[enumerable]]为false
Object.defineProperty(SpecialArray.prototype, "constructor", {
  enumerable: false,
  value: SpecialArray
});

SpecialArray.prototype.toPipedString = function() {
  return this.join("|");
};

var arr = SpecialArray(1, 2, 3); // 不需要new

console.log(arr.toPipedString()); // 1|2|3
console.log(arr instanceof SpecialArray) // true

我们看到arr.toPipedString()可以返回正确的值了,且arr instanceof SpecialArray为true,即完成了继承。这种做法恰好和原型链继承相反,原型链继承是将父类实例作为子类的原型,而该方法是将父类实例的原型指针指向了子类的原型。但是,这种方法有一个很大的问题:__proto__属性是一个非标准属性,其在部分安卓机上未被实现,因此就有一种说法:ES5及以下的JS无法完美继承数组。

es6 extends

es6的extends其实能够很方便的帮我们完成Array继承:

class SpecialArray extends Array {
  constructor(...args) {
    super(...args)
  }

  toPipedString() {
    return this.join("|");
  }
}

var arr = new SpecialArray(1, 2, 3)

console.log(arr.toPipedString()) // 1|2|3
console.log(arr instanceof SpecialArray) // true

因为我们调用super的时候是先新建父类的实例this,然后再用子类的构造函数SpecialArray来修饰this,这是es5当中做不到的一点。

vue中的数组

我们知道在vue中,push、pop、splice等方法可以触发响应式更新,而arr[0] = 1这种写法无法触发,原因是defineProperty无法劫持数组类型的属性,那么vue是如何让常用的方法触发更新的呢,我们看:

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);

var methodsToPatch = [
  ‘push‘,
  ‘pop‘,
  ‘shift‘,
  ‘unshift‘,
  ‘splice‘,
  ‘sort‘,
  ‘reverse‘
];

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case ‘push‘:
      case ‘unshift‘:
        inserted = args;
        break
      case ‘splice‘:
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify();
    return result
  });
});

这是vue的部分源码,我们不用细看,看重点即可。我们可以看到vue创建了一个对象arrayMethods,它是以Array.prototype作为原型的。然后改写了arrayMethods中的push、pop、shift等方法,即在原有功能的基础上触发ob.dep.notify()完成更新。那它是如何将我们声明的数组指向arrayMethods的呢,我们继续看:

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, ‘__ob__‘, this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};
/**
 * Augment an target Object or Array by intercepting
 * the prototype chain using __proto__
 */
function protoAugment (target, src, keys) {
  /* eslint-disable no-proto */
  target.__proto__ = src;
  /* eslint-enable no-proto */
}

/**
 * Augment an target Object or Array by defining
 * hidden properties.
 */
/* istanbul ignore next */
function copyAugment (target, src, keys) {
  for (var i = 0, l = keys.length; i < l; i++) {
    var key = keys[i];
    def(target, key, src[key]);
  }
}

我们看到vue先是做了个判断,即当前运行环境是否支持__proto__属性。若支持,执行protoAugment(),将target的__proto__指向arrayMethods,这其实就是我们上面实现的es5的继承方式。若不支持,就将arrayMethods里的方法注入到target中完成mixin的操作。

总结

寄生组合式继承虽然很完美,但是它没办法做到继承原生类型的构造函数,此时可以借用我们实现的进化版的寄生构造函数模式完成继承。每个阶段回头去看一些基础总会发现有不同的收获,这次的分享内容也是看了js高级程序设计引发的一些思考。因此,百忙之中,我们也需要经常去温习基础知识,所谓温故而知新,正是如此。

原文地址:https://www.cnblogs.com/danceonbeat/p/10704792.html

时间: 2024-11-02 09:53:17

关于js中原生构造函数的继承的相关文章

js中的原型、继承的一些想法

最近看到一个别人写的js类库,突然对js中的原型及继承产生了一些想法,之前也看过其中的一些内容,但是总不是很清晰,这几天利用空闲时间,对这块理解了一下,感觉还是有不通之处,思路上没那么条理,仅作为分享, 一.instanceof 在JavaScript有instanceof运算符,是二元运算符,使用方法 instanceA instanceof A,返回值是布尔值(boolean),含义是判断instanceA是否是A的一个实例,其实质是判断A.prototype===instanceA.__p

JS中有关对象的继承以及实例化、浅拷贝深拷贝的奥秘

一.属性的归属问题 JS对象中定义的属性和方法如果不是挂在原型链上的方法和属性(直接通过如类似x的方式进行定义)都只是在该对象上,对原型链上的没有影响.对于所有实例共用的方法可直接定义在原型链上这样实例化的的时候就不用对每个实例定义该属性方法,所有的实例均具有该方的引用见最后的输出. function Myclass(){ this.x=" x in Myclass"; this.get=function(){}//每次实例化对象,每个对象的该方法都是独立的,是不相同的 } Mycla

JS中的原型与继承

原型: Js所有的函数都有一个prototype属性,这个属性引用了一个对象,即原型对象,也简称原型.这个函数包括构造函数和普通函数,我们讲的更多是构造函数的原型,但是也不能否定普通函数也有原型.譬如普通函数: function F(){ ;}alert(F.prototype instanceof Object) //true 构造函数实例化对象的过程. function A(x){ this.x=x; } var obj=new A(1); 实例化obj对象有三步: 1. 创建obj对象:v

js中原生对象、内置对象和宿主对象(转)

本帖最后由 无解. 于 2012-9-9 12:13 编辑 <ignore_js_op> 这个图来自于<JavaScript语言精髓与编程实践>第三章P184页.最近在改第二版,这张图重做了,需要的可以对照着看. 此外,补充一下图中用到的概念: 1.内置(Build-in)对象与原生(Naitve)对象的区别在于:前者总是在引擎初始化阶段就被创建好的对象,是后者的一个子集:而后者包括了一些在运行过程中动态创建的对象. 2.引擎扩展对象是一个并不太大的集合,一般来说比较确定,它们也属

js中new构造函数的研究

<javascript高级编程>里对new操作符的解释: new操作符会让构造函数产生如下变化: 1.       创建一个新对象: 2.       将构造函数的作用域赋给新对象(因此this就指向了这个新对象): 3.       执行构造函数中的代码(为这个新对象添加属性): 4.       返回新对象 /* Constr:构造函数 args:初始化参数 */ function newOperator(Constr, args) { var thisValue = Object.cre

浅谈JS中的构造函数、原型对象(prototype)、实例中的属性/方法之间的关系

原文链接:https://segmentfault.com/a/1190000016951069 构造函数:函数中的一种,通过关键字new可以创建其实例.为了便于区分,通常首字母大写:原型对象:一种特殊的对象,构造函数创建时自动生成:与构造函数形成一一对应,如同人和影子般的关系:实例:通过构造函数实例出来的对象: 在定义构造函数时,在其内部(“{“和”}”)进行定义属性和方法.当我们通过关键字new,对构造函数进行实例化的时候.实例会对构造函数的这些属性进行拷贝出一份副本,然后将其归属为当前实例

js中,类的继承

创建类:class student{    constructor(name,age){        this.name=name;        this.age=age;    }    learn(){        console.log('我是学生');    }}类的继承:class child extends Student{    constructor(name,age){        super (name,age);    }    coolings(){      

js中的继承问题

1.继承的概念:把别人的拿过来变成自己的,但自己不受影响. 2.js中最基本的继承就是原型继承. 3.原型继承:通过修改子级构造函数的prototype指向父级构造函数的实例对象. function Animal(name){ this.name=name; this.favor=['eating','sleeping']; } Cat.prototype=new Animal('Kitty'); function Cat(color){ this.color=color; } var cat=

详细理解JS中的继承

正式说继承之前,有两个相关小点: JS只支持实现继承,即继承实际的方法,不支持接口继承(即继承方法的签名,但JS中函数没签名) 所有对象都继承了Object.prototype上的属性和方法. 说继承之前还要再说一下原型.原型之所以很重要,原因之一就是可以利用它来实现JavaScript的继承.重写一个函数的原型对象,将其指定为另一个函数的实例,这样便实现了一个简单的原型链继承. 看一个利用原型链来实现继承的基础例子: 1 function Super(){ 2 this.name='super