Skip to content

类相关 #9

@coconilu

Description

@coconilu

前言

在JS的世界中,是没有严格的类这个概念的,但它确实是一门面向对象语言。

  1. 封装,JS的对象就是由一些键值对数据组成,封装了数据和方法
  2. 继承,虽然没有类这个概念,但是可以通过new和原型链实现继承
  3. 多态,由于JS是动态弱类型语言,所以是很容易实现的

鸭子类型(duck typing)如果它走起路来像鸭子,叫起来也是鸭子,那么它就是鸭子。

prototype

前面的文章说过,JS引擎线程用堆来存储执行过程中创建的对象(函数也是对象),每个函数对象都有prototype属性,每个prototype默认有两个属性:constructor(指向函数自己)和__proto__指向下一个原型。
如果一个函数对象被当做构造函数使用,也就是使用new语法糖调用函数对象,那么它的prototype会称为新生成的对象的原型(proto)。

new的原理

1. 构造函数

JS没有严格的类概念,它是通过一个(构造)函数加上new语法糖去生成一个新对象的。
大致过程如下:

  1. 创建一个新的空对象
  2. 把新对象的__proto__指向构造函数的prototype(默认的prototype就有constructor指向构造函数,还有__proto__指向下一个原型链);也可以描述成使用setPrototypeOf()方法设置新对象的原型
  3. 对该构造函数使用call或apply方法,传入该新对象(作为this)和构造函数的参数
  4. 如果构造函数没有返回对象(普通对象、函数对象、数组),则返回这个新对象

如果构造函数返回了非对象(!result instanceof Object),并不影响返回this指向的新对象。

new

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. 防止构造函数被直接调用

由于构造函数和普通函数一样都是可以直接调用的。
所以为了防止构造函数被当做普通函数使用,下面有两种方案可以选择。

  1. 上面的描述中,第三步执行构造函数之前就已经把新对象的__proto__设置成了构造函数的prototype,所以可以借助instanceof判断新对象的原型链上是不是有构造函数的prototype
function Bar(params) {
    if (!(this instanceof Bar)) {
      return new Bar(params);
    }
}
  1. 还可以借助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();

缺点:

  1. 来自原型对象的所有属性被所有实例共享
  2. 创建子类实例时,无法向父类构造函数传参

2. 构造继承

描述:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)

function Parent () {};
function Child () {
    Parent.call(this);
};
var child = new Child();

缺点:

  1. 实例并不是父类的实例,只是子类的实例
  2. 只能继承父类的实例属性和方法,不能继承原型属性/方法
  3. 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能

3. 实例继承

描述:为父类实例添加新特性,作为子类实例返回

function Parent () {};
function Child (prop) {
    var instance = new Parent ();
    instance.prop = prop || 'default_prop';
    return instance;
}

缺点:

  1. 实例是父类的实例,不是子类的实例

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;
}

缺点:

  1. 效率较低,内存占用高
  2. 无法获取父类不可枚举的方法

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的代理

关键源码(2.5.27版本)

我把关键源码抽取出来:

// 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的详细信息。
extend

写在最后

博客如有哪里不对,请不吝赐教。

参考链接

new.target
mqyqingfeng/Blog#16
Object.create
ES6 入门,阮一峰
JS实现继承的几种方式

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions