-
Notifications
You must be signed in to change notification settings - Fork 20
Description
前言
在JS的世界中,是没有严格的类这个概念的,但它确实是一门面向对象语言。
- 封装,JS的对象就是由一些键值对数据组成,封装了数据和方法
- 继承,虽然没有类这个概念,但是可以通过new和原型链实现继承
- 多态,由于JS是动态弱类型语言,所以是很容易实现的
鸭子类型(duck typing)如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。
prototype
前面的文章说过,JS引擎线程用堆来存储执行过程中创建的对象(函数也是对象),每个函数对象都有prototype属性,每个prototype默认有两个属性:constructor(指向函数自己)和__proto__指向下一个原型。
如果一个函数对象被当做构造函数使用,也就是使用new语法糖调用函数对象,那么它的prototype会称为新生成的对象的原型(proto)。
new的原理
1. 构造函数
JS没有严格的类概念,它是通过一个(构造)函数加上new语法糖去生成一个新对象的。
大致过程如下:
- 创建一个新的空对象
- 把新对象的__proto__指向构造函数的prototype(默认的prototype就有constructor指向构造函数,还有__proto__指向下一个原型链);也可以描述成使用
setPrototypeOf()方法设置新对象的原型 - 对该构造函数使用call或apply方法,传入该新对象(作为this)和构造函数的参数
- 如果构造函数没有返回对象(普通对象、函数对象、数组),则返回这个新对象
如果构造函数返回了非对象(!result instanceof Object),并不影响返回this指向的新对象。
2. instanceof
借用MDN的一句话
instanceof 运算符用来测试一个对象(A)在其原型链中是否存在一个构造函数(B)的 prototype 属性。
用法:
A instanceof B
1. 这里的A是对象,如果是基本类型的话,是找不到它的原型的
0 instanceof Number // false
new Number(0) instanceof Number // true
2. 对象A会查看自己的__proto__属性指向的是不是B的prototype,如果不是则继续往下——查看A的__proto__的__proto__是不是指向B的prototype, 遍历直到找到返回true,或者遍历结束返回false。
我们用原型链继承的例子解释这个过程:
function Parent () {};
function Child () {};
Child.prototype = new Parent();
var child = new Child();
child instanceof Child; // true
child instanceof Parent; // true
isPrototypeOf() 方法允许你检查一个对象是否存在于另一个对象的原型链上。
3. 即使一个构造函数的prototype属性中的constructor属性没有指向自己也不会影响instanceof的结果
3. 防止构造函数被直接调用
由于构造函数和普通函数一样都是可以直接调用的。
所以为了防止构造函数被当做普通函数使用,下面有两种方案可以选择。
- 上面的描述中,第三步执行构造函数之前就已经把新对象的__proto__设置成了构造函数的prototype,所以可以借助
instanceof判断新对象的原型链上是不是有构造函数的prototype
function Bar(params) {
if (!(this instanceof Bar)) {
return new Bar(params);
}
}
- 还可以借助
new.target来判断构造函数是不是通过new被调用的
function Foo() {
if (!new.target) throw "Foo() must be called with new";
console.log("Foo instantiated with new");
}
继承的几种方式
1. 原型链继承
描述:将父类的实例作为子类的原型
function Parent () {};
function Child () {};
Child.prototype = new Parent();
var child = new Child();
缺点:
- 来自原型对象的所有属性被所有实例共享
- 创建子类实例时,无法向父类构造函数传参
2. 构造继承
描述:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Parent () {};
function Child () {
Parent.call(this);
};
var child = new Child();
缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3. 实例继承
描述:为父类实例添加新特性,作为子类实例返回
function Parent () {};
function Child (prop) {
var instance = new Parent ();
instance.prop = prop || 'default_prop';
return instance;
}
缺点:
- 实例是父类的实例,不是子类的实例
4. 拷贝继承
描述:每次构造新对象的时候,给新对象的原型添加同名的父类可枚举属性(包括原型链)
function Parent () {};
function Child (prop) {
var instance = new Parent ();
for (var p in Parent) {
Child.prototype[p] = Parent[p];
}
instance.prop = prop || 'default_prop';
return instance;
}
缺点:
- 效率较低,内存占用高
- 无法获取父类不可枚举的方法
5. 组合继承(原型链+构造函数)
描述:通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Parent () {};
function Child () {
Parent.call(this);
};
Child.prototype = new Parent();
var child = new Child();
缺点:
调用了两次父类构造函数,生成了两份实例
6. 寄生组合继承
描述:子类实例通过构造函数继承父类的属性,子类实例的原型是以父类原型作为原型的空对象
function Parent () {};
function Child () {
Parent.call(this);
};
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
var child = new Child();
对比组合继承,寄生组合继承避免创建了一些不必要的、多余的属性。因为组合继承使用new Parent()创建的新对象设置为子类的prototype,该新对象还包括一些冗余的属性。
7. 总结几种继承方式
所以,寄生组合式继承是目前业内公认最高效整洁的继承方式。
可能原型式继承和寄生式继承太受欢迎了吧,ES5实现了一个api:Object.create(proto, propertiesObject):
// Polyfill
if (typeof Object.create !== "function") {
Object.create = function (proto, propertiesObject) {
// 这里省略一些安全检查
var o;
function F() {};
F.prototype = proto;
o = new F();
o.defineProperties(propertiesObject)
return o
}
}
行为委托
JavaScript 本身就没有类似“类”的抽象机制。基于原型的方式相对于面向类编程,更适合叫“对象关联”(OLOO,objects linked to other objects)。
摘一段来自《你不知道的JavaScript》上卷里“行为委托”章节里的代码:
Task = {
setID: function (ID) { this.id = ID; },
outputID: function () { console.log(this.id); }
};
// 让XYZ 委托Task
XYZ = Object.create(Task);
XYZ.prepareTask = function (ID, Label) {
this.setID(ID);
this.label = Label;
};
XYZ.outputTaskDetails = function () {
this.outputID();
console.log(this.label);
};
代理模式
可以代理处理对某个对象的操作。跟设计模式里的代理模式类似,可以设置拦截属性查找,赋值,枚举,函数调用等。
下面的代码展示使用Proxy来实现前面的行为委托:
let person = {
name: 'Bar'
}
let handler = {
get: function (target, name) {
if (name === 'getName') {
return () => target['name']
}
}
}
let p = new Proxy(person, handler)
console.log(p.getName()) // Bar
当然Proxy还可以实现更多的功能,推荐MDN的权威链接。
VueJs的代理
我把关键源码抽取出来:
// initData 方法里有如下代码,说明我们传给Vue构造函数的options.data被绑定到了vm._data
let data = vm.$options.data
data = vm._data = typeof data === 'function' ?
getData(data, vm) :
data || {}
// 通关代理的方式,使vm[key] === vm._data[key]
const sharedPropertyDefinition = {
enumerable: true,
configurable: true,
get: noop,
set: noop
}
export function proxy(target: Object, sourceKey: string, key: string) {
sharedPropertyDefinition.get = function proxyGetter() {
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter(val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
proxy(vm, `_data`, key) // 遍历设置代理
借助Object.defineProperty就可以把vm[key]的getter和setter绑定到vm._data[key]上,而options.data和vm._data是指向同一个对象的。
官网有如下一个例子指出了这一点:
// 我们的数据对象
var data = { a: 1 }
// 该对象被加入到一个 Vue 实例中
var vm = new Vue({
data: data
})
// 它们引用相同的对象!
vm.a === data.a // => true
ES6
即使在ES6里,有了class、extend关键字,但是JS的本质还是没有改变。
1. class
语法糖class,规定必须有一个constructor(如果没有系统会自动添加一个默认的constructor)和若干个函数声明。
其中的constructor就是构造函数;
若干个函数声明会转变为构造函数的prototype上的函数。
class A {
constructor () {/**constructor code**/}
foo () {/**foo code**/}
}
会被转化为下面:
var A = function () {
function A () {/**constructor code**/}
A.prototype.foo = function () {/**foo code**/}
return A
}
2. extend
也是一个语法糖,原理是上面提到的寄生组合式继承。
可以通过一段代码来验证:
class Father {
constructor() {
this.fatherSelf = function () { }
}
fatherPrototype() { }
}
class Child extends Father {
constructor() {
super()
this.childSelf = function () { }
}
childPrototype() { }
}
var child = new Child()
上面的代码可以直接在chrome的开发者工具里运行,然后查看c的详细信息。

写在最后
博客如有哪里不对,请不吝赐教。
参考链接
new.target
mqyqingfeng/Blog#16
Object.create
ES6 入门,阮一峰
JS实现继承的几种方式
