关于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"
对于这种模式有诸多不解:
- 仔细一看,这特么不就是所谓的工厂函数模式吗?工厂模式的几个缺点它都存在,一种是创建的所有对象均为Object类型,无法进行类型识别;其次每次创建对象都会重新生成一个function用来创建sayName属性,浪费内存。
- 这里的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绑定
上面是典型的寄生组合式继承的写法,其存在几个问题:
- new的行为上面介绍过,它会返回对象类型,而我们的SpecialArray希望像Array一样,new的时候返回数组。
- 我们先通过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。这样会造成两个问题:
- 无法通过instanceof确定实例的类型,它始终为Array的实例
- 我们希望将构造函数的方法放入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高级程序设计引发的一些思考。因此,百忙之中,我们也需要经常去温习基础知识,所谓温故而知新,正是如此。
关于js中原生构造函数的继承的更多相关文章
- js中的几种继承方法
JS作为面向对象的弱类型语言,继承也是其非常强大的特性之一. 继承:子承父业:一个原本没有某些方法或属性的对象,统一写方法,拿到了另外一个对象的属性和方法 下面是js中的几种继承方式 1.改变this ...
- js中的原型、继承的一些想法
最近看到一个别人写的js类库,突然对js中的原型及继承产生了一些想法,之前也看过其中的一些内容,但是总不是很清晰,这几天利用空闲时间,对这块理解了一下,感觉还是有不通之处,思路上没那么条理,仅作为分享 ...
- 详解js中的寄生组合式继承
寄生组合式继承是js中最理想的继承方式, 最大限度的节省了内存空间. js中的寄生组合式继承要求是: 1.子对象有父对象属性的副本, 且这些不应该保存在子对象的prototype上. 2. ...
- js面向对象(构造函数与继承)
深入解读JavaScript面向对象编程实践 Mar 9, 2016 面向对象编程是用抽象方式创建基于现实世界模型的一种编程模式,主要包括模块化.多态.和封装几种技术. 对JavaScript而言,其 ...
- JS中有关对象的继承以及实例化、浅拷贝深拷贝的奥秘
一.属性的归属问题 JS对象中定义的属性和方法如果不是挂在原型链上的方法和属性(直接通过如类似x的方式进行定义)都只是在该对象上,对原型链上的没有影响.对于所有实例共用的方法可直接定义在原型链上这样实 ...
- JS中5种经典继承方式
继承 JS中继承的概念: 通过[某种方式]让一个对象可以访问到另一个对象中的属性和方法,我们把这种方式称之为继承 并不是所谓的xxx extends yyy 为什么要使用继承? 有些对象会有方法(动作 ...
- JS中关于构造函数、原型链、prototype、constructor、instanceof、__proto__属性
在Javascript不存在类(Class)的概念,javascript中不是基于类的,而是通过构造函数(constructor)和原型链(prototype chains)实现的.但是在ES6中引入 ...
- Js中关于构造函数,原型,原型链深入理解
在 ES6之前,在Javascript不存在类(Class)的概念,javascript中不是基于类的,而是通过构造函数(constructor)和原型链(prototype chains)实现的.但 ...
- js中的封装、继承、多态
Javascript是一门解释型的语言,是基于对象的,并不是真正的面向对象的语言,对变量类型的应用也是宽松的,其实它同样可以模拟面向对象的功能: 1 function myfun1(){ 2 ...
随机推荐
- 如何在linux环境安装数据库
1.1 获取oracle 数据库安装包: 注意:获取的是database的安装包,不是客户端的安装包 1.2 以root用户登陆云主机,修改主机名 Hostname 1.2.1 ...
- 阻止事件冒泡传播stopPropagation() 阻止自身默认行为preventdefault()
stopPropagation 简单理解:子元素的点击事件 不会去触发父元素的点击事件 preventdefault 简单理解:当点击提交按钮时(submit) 阻止对表 ...
- mysql 数据库主从同步
1.简介 写这篇文章是网上找到的相关主从同步的都不够完全,本人第一次搭建主从同步,完全看着网上的文章来搭建的,结果你懂的,踩了很多坑.所以特地把踩到的坑写出来,新手切勿直接布置到正式环境,请于测试环境 ...
- hdu-3689 Infinite monkey theorem 概率dp+kmp
有一只猴子随机敲键盘,给出它可能敲的键以及敲各个键的概率. 输入:n,表示有多少个键,m,表示猴子会敲m次键 n个二元组(字母,数字) 表示键代表的字母及其被敲的概率. 最后一个目标字符串. 问这只猴 ...
- 七、Django模型基础第二节——常用查询
1 常用的模型字段类型 官方文档链接: https://docs.djangoproject.com/en/2.1/ref/models/fields/#field-types 常用的字段类型 模型字 ...
- Linux matlab.desktop文件
matlab建立菜单文件老是闪退,须用 matlab -desktop 完整配置 matlab.desktop [Desktop Entry] Name=MATLAB Exec=/usr/local/ ...
- JavaScript新手入门 贪吃蛇
从小就在玩贪吃蛇,但是知道今天自己做了一遍才知道原理的具体的实现步骤. 刚进入界面时显示开始游戏(不重要,本人比较喜欢吹毛求疵) 中间黑色部分为游戏的主要展示部分 主要步骤及源码: body中代码,红 ...
- 12集合(1)-----List
一.总体分类 Collection(包括方法add,remove,contains,clear,size) List(接口) LinkedList ArrayList Vector---Stack 2 ...
- 数据库中无数据时查询数据为空,但出现空指针异常的解决方案(转载https://blueskator.iteye.com/blog/2096026)
异常:java.lang.NullPointerException 解决方案: try{ hotelImageList = ihotelImgManager.query(hiqc); }catch(E ...
- Eclipse导入已有的项目后项目报错的解决办法
第一种:jsp报错 选择windows-->preference-->列表找到Validation-->点击Disable All ->> Apply ->> ...