8000 JS的继承的总结 · Issue #15 · phenomLi/Blog · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

JS的继承的总结 #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
phenomLi opened this issue Jan 26, 2018 · 0 comments
Open

JS的继承的总结 #15

phenomLi opened this issue Jan 26, 2018 · 0 comments

Comments

@phenomLi
Copy link
Owner
phenomLi commented Jan 26, 2018

Javascript中的继承一直是一个难点,因为js有别于其他面向对象的语言,js是基于 原型(prototype)
的。

prototype是一个很难琢磨得透也很难掌握的东西,当然也许有人会跳出来说现在都用ES6,typescript啦,谁还学prototype。这样想就错了,首先,ES6和typescript远远没有你想的这么普及,谁敢把不经过编译的es6和ts直接放到线上跑?编译后还不是一样回到prototype。其次,prototype能干的事情多的去了,不单单只是new或继承几个类,有兴趣可以看看Vue的响应式数组方法是怎么做的。

扯远了,今天写这篇东西是因为刚刚看到了一篇关于js继承的文章,想把一些思考和总结记下来。


如何实现继承

为什么在js中继承很麻烦?

  • 因为js中没有extends关键字(ES6前)

  • 因为js既能访问到实例属性也能访问到原型属性

  • js对象是引用类型


一点一点来分析。

首先在ES6之前,js中是没有又甜又可爱的extends语法糖的,那要继承怎么办,只能自己在现有的js语法里面各种找方法实现(而且还不好找)。

其次,在一个js对象中,既有来自构造函数的属性,也能访问到其_proto_,也就是构造函数的prototype的属性(通常是方法)。这么理解这句话呢?看下面的例子:

const Foo = function() {
    this.count = 20;
};

Foo.prototype.getTotal = function() {
    return 400;
};

const foo = new Foo();

console.log(foo.count); //输出200
console.log(foo.getTotal()); //输出400
console.log(foo.__proto__.getTotal()); //输出400

/*
* 输出 { count: 20 }
* 可以看到在foo中并没有getTotal这个方法
*/
console.log(foo);  

/*
* 输出 { getTotal: [Function] }
* 而getTotal是在__proto__中 
*/
console.log(foo.__proto__);  

可以看到,getTotal是绑定在foo的构造函数的prototype中的一个方法,在实例化Foo后,foo既能访问它自身的属性count,也能访问getTotal方法。但是getTotal方法并没有在foo对象里面,所以很明显,当要访问一个对象的某个属性/方法时,js引擎首先在对象里面找,如果找不到,再顺着原型链往上找。foofoo.__proto__关系如下:

如果对__proto__和prototype的关系不了解,或者对原型链有疑惑的,建议先去了解一下,本篇文章不会细讲。


那么,也就是说,想要在js中继承一个类,就必须要做到两点:

  1. 子类要继承父类中的属性/方法

  2. 子类的prototype要继承父类的prototype

做不到第二点的都不是完整继承。


第一点的常规实现方法是:

//父类
const SuperClass = function() {
    this.a = 1;
};

//子类
const SubClass = function() {

    /*
    * 很巧妙的一直做法,因为父类的属性都定义在构造函数里面,
    * 所以只要在子类的构造函数里面用子类的上下文(this)调用一下父类的构造函数,
    * 父类的属性就都绑定到了子类的this上面去了
    */
    SuperClass.call(this);
}

//实例化子类
const sub = new SubClass();

console.log(sub.a);  //输出1

在子类的构造函数里面用一下call(当然也可以用apply,个人比较喜欢用call)调用父类的构造函数就行。

然后第二点,思路是这样子:

//简单粗暴地将父类的prototype指向子类的prototype
SubClass.prototype = SuperClass.prototype;

由于prototype是对象而不是函数,所以没法用call或者apply的方法了。

但是事情没有这么简单,仔细观察上面的代码:

SubClass.prototype = SuperClass.prototype;

发现问题了吗?js中的对象都是引用类型(对什么是引用类型不了解的可以看看这篇文章:JS中的深拷贝),对象的直接赋值都是改变指针指向的地址,也就是说:

子类的prototype和父类的prototype共享同一片内存空间了。

会造成什么问题?会造成当你想要往子类的prototype里添加属性/方法的时候,父类的prototype也会被修改:

//往子类的prototype添加了一个属性b
SubClass.prototype.b = 'phenom';
//父类的prototype也被加上了
console.log(SuperClass.prototype.b); //输出 phenom

这显然不好,但是怎么改进?不就是想要有独立分配的内存空间嘛,很简单,我们有父类SuperClass,我们直接让子类的 prototype 指向父类的实例:

//往父类的prototype里添加一个方法getA
SuperClass.prototype.getA = function() {
    return this.a;
};

//子类的prototype指向父类的一个实例
SubClass.prototype = new SuperClass();

//实例化一个子类
const sub = new SubClass();

console.log(sub.getA());  //输出1

为什么这种方法可以?其实父类的实例里面是没有getA这个方法的,getA在父类的prototype里面,但是前面已经说过了,js找一个对象的属性/方法是会顺着原型链往上找的:

/**
 * 输出 { a: 1 },没有getA方法
 */
console.log(SubClass.prototype);

/**
 * 输出 { getA: [Function] },getA方法在这里
 * 顺着原型链找到了这里
 */
console.log(SubClass.prototype.__proto__);

目前为止他们的恩怨情仇大概是这个样子:




看上图不难发现,有一个地方貌似不太合理(我故意画出来了),就是子类的prorotypeconstructor指针居然指向了父类的构造函数:

/**
 * 输出 [Function: SuperClass]
 * 原因是我们将父类实例赋值给子类的prototype时,把其constructor属性也一同覆盖了
 * 正常情况下子类的prototype的constructor应该指向子类的构造函数的
 */
console.log(SubClass.prototype.constructor);  

所以我们要做一个修正:

//修正
SubClass.prototype.constructor = SubClass;


寄生组合继承

我们把上面的所有实现都糅合起来,放进一个函数里面,并且命名为myExtends(不能直接用extends因为是保留字):

//父类
const SuperClass = function() {
    this.a = 1;
};
SuperClass.prototype.getA = function() {
    return this.a;
};

//子类
const SubClass = function() {
    SuperClass.call(this);
}

const myExtends = function(subClass, superClass) {
    //子类的prototype指向父类的一个实例
    subClass.prototype = new superClass();

    //修正
    subClass.prototype.constructor = subClass;

    return subClass;
};

myExtends(SubClass, SuperClass);

//实例化一个子类
const sub = new SubClass();

这样看起来清晰多了,但是还不完美。上面的代码中,父类的构造函数SuperClass一共被调用了两次,这不是好事,我们要想办法优化一下。

想一下,其实子类的prototype并不关心父类的构造函数中定义了哪些内容,因为父类构造函数中的内容已经在SuperClass.call(this);中继承给了子类的构造函数,我们要的只是父类prototype的内容,所以subClass.prototype = new superClass();是产生了一点冗余的,但是又不能直接赋值,因为父子两个类的prototype需要有独立的内存空间。所以,我们可以找一个能提供独立空间存放父类prototype的‘中间人’:

const myExtends = function(subClass, superClass) {
    //中间人函数
    const F = function() {};

    //存放父类的prototype
    F.prototype = superClass.prototype;

    /**
     * 子类的prototype指向中间人函数的一个实例,具有独立的内存空间,
     * 但是内存占用非常小,因为这个函数是没有内容的,而且这个中间人函数外界没法修改,
     * 所以也不会影响父类的prototype
     */
    subClass.prototype = new F();

    //修正
    subClass.prototype.constructor = subClass;

    return subClass;
};

这样就完美了。




下面贴上完整代码:

//父类
const SuperClass = function() {
    this.a = 1;
};
SuperClass.prototype.getA = function() {
    return this.a;
};

//子类
const SubClass = function() {
    SuperClass.call(this);
}

const myExtends = function(subClass, superClass) {
    //中间人函数
    const F = function() {};

    //存放父类的prototype
    F.prototype = superClass.prototype;

    /**
     * 子类的prototype指向中间人函数的一个实例,具有独立的内存空间,
     * 但是内存占用非常小,因为这个函数是没有内容的,而且这个中间人函数外界没法修改,
     * 所以也不会影响父类的prototype
     */
    subClass.prototype = new F();

    //修正
    subClass.prototype.constructor = subClass;

    return subClass;
};

myExtends(SubClass, SuperClass);

//实例化一个子类
const sub = new SubClass();

console.log(sub.getA());  //输出 1

其实上面这种实现,就是js中使用最普遍的寄生组合继承的实现。结合了各种继承的优点,而且实现起来也比较简单,容易理解。




--EOF--

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant
0