Skip to content
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

JavaScript深入之继承的多种方式和优缺点 #16

Open
mqyqingfeng opened this issue May 12, 2017 · 117 comments
Open

JavaScript深入之继承的多种方式和优缺点 #16

mqyqingfeng opened this issue May 12, 2017 · 117 comments

Comments

@mqyqingfeng
Copy link
Owner

@mqyqingfeng mqyqingfeng commented May 12, 2017

写在前面

本文讲解JavaScript各种继承方式和优缺点。

但是注意:

这篇文章更像是笔记,哎,再让我感叹一句:《JavaScript高级程序设计》写得真是太好了!

1.原型链继承

function Parent () {
    this.name = 'kevin';
}

Parent.prototype.getName = function () {
    console.log(this.name);
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

console.log(child1.getName()) // kevin

问题:

1.引用类型的属性被所有实例共享,举个例子:

function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {

}

Child.prototype = new Parent();

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy", "yayu"]

2.在创建 Child 的实例时,不能向Parent传参

2.借用构造函数(经典继承)

function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {
    Parent.call(this);
}

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy"]

优点:

1.避免了引用类型的属性被所有实例共享

2.可以在 Child 中向 Parent 传参

举个例子:

function Parent (name) {
    this.name = name;
}

function Child (name) {
    Parent.call(this, name);
}

var child1 = new Child('kevin');

console.log(child1.name); // kevin

var child2 = new Child('daisy');

console.log(child2.name); // daisy

缺点:

方法都在构造函数中定义,每次创建实例都会创建一遍方法。

3.组合继承

原型链继承和经典继承双剑合璧。

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {

    Parent.call(this, name);
    
    this.age = age;

}

Child.prototype = new Parent();
Child.prototype.constructor = Child;

var child1 = new Child('kevin', '18');

child1.colors.push('black');

console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]

var child2 = new Child('daisy', '20');

console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]

优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。

4.原型式继承

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。

缺点:

包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。

var person = {
    name: 'kevin',
    friends: ['daisy', 'kelly']
}

var person1 = createObj(person);
var person2 = createObj(person);

person1.name = 'person1';
console.log(person2.name); // kevin

person1.firends.push('taylor');
console.log(person2.friends); // ["daisy", "kelly", "taylor"]

注意:修改person1.name的值,person2.name的值并未发生改变,并不是因为person1person2有独立的 name 值,而是因为person1.name = 'person1',给person1添加了 name 值,并非修改了原型上的 name 值。

5. 寄生式继承

创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。

function createObj (o) {
    var clone = Object.create(o);
    clone.sayName = function () {
        console.log('hi');
    }
    return clone;
}

缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。

6. 寄生组合式继承

为了方便大家阅读,在这里重复一下组合继承的代码:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

Child.prototype = new Parent();

var child1 = new Child('kevin', '18');

console.log(child1)

组合继承最大的缺点是会调用两次父构造函数。

一次是设置子类型实例的原型的时候:

Child.prototype = new Parent();

一次在创建子类型实例的时候:

var child1 = new Child('kevin', '18');

回想下 new 的模拟实现,其实在这句中,我们会执行:

Parent.call(this, name);

在这里,我们又会调用了一次 Parent 构造函数。

所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为colors,属性值为['red', 'blue', 'green']

那么我们该如何精益求精,避免这一次重复调用呢?

如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢?

看看如何实现:

function Parent (name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child (name, age) {
    Parent.call(this, name);
    this.age = age;
}

// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();


var child1 = new Child('kevin', '18');

console.log(child1);

最后我们封装一下这个继承方法:

function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}

// 当我们使用的时候:
prototype(Child, Parent);

引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是:

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。

相关链接

《JavaScript深入之从原型到原型链》

《JavaScript深入之call和apply的模拟实现》

《JavaScript深入之new的模拟实现》

《JavaScript深入之创建对象》

深入系列

JavaScript深入系列目录地址:https://github.com/mqyqingfeng/Blog

JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。

@jawil
Copy link

@jawil jawil commented May 12, 2017

博主还没写事件循环和浏览器渲染机制这块,借宝地这里有个问题提前探讨一下。

<html>
<body>
    <h2>Hello</h2>
    <script>
    function printH2() {
        console.log('first script', document.querySelectorAll('h2'));
    }
    printH2()
    setTimeout(printH2)
    </script>
    <link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/4.0.0-alpha.4/css/bootstrap.css">
    <h2>World</h2>
    <script>
    console.log('second script');
    </script>
</body>
</html>

这个demo的效果是:控制台先打印出来了printH2(),setTimeout(printH2)的结果(说明此时的DOMtree已经完成),然后浏览器显示了页面,页面完全显示之后(RenderTree完成)才执行了 console.log('second script');

有个地方不明白,就是js的异步任务与UI线程之间的调度,这个demo的结果来看,DOM节点渲染之后生成DOMtree才开始执行 setTimeout(printH2)的printH2函数,所以打印出了两个h2节点。
博主能帮忙解答一下UI线程和异步任务之间的关系到底是怎么样的吗?或者对于这一块有什么好的文章学习吗

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 12, 2017

哈哈,这道题难住我了,对这个方面并没有研究,抱歉也提供不了什么资料,不过我会把这个课题记下来,如果想明白了或者有所进展,立刻跟题主讨论哈~

@shaopower
Copy link

@shaopower shaopower commented May 12, 2017

@jawil ,其实不一定是 setTimeout(printH2) 早于 second script 的
要看 bootstrap.css 这个文件获取的时机,如果 css 已经被浏览器缓存着的话(譬如第二次访问) second script 可能会先于 setTimeout。

照理说second script 应该是会被 css 阻塞执行的。不过之前都是看的 css 在 head 里的表现,放在 body 里是不是也有阻塞一说不太清楚。

只是我测下来的现象 disable cache 的话怎么刷新 second script 肯定在最后,但是把 disable cache 勾选去掉的话,后两个出现的顺序不固定。

浏览器里面的线程也希望有个大神能分享下,之前也遇到过很奇怪的表现,譬如 js 早于 css 或者不晚于 css 10ms 以内,浏览器会等 js 执行完再渲染;如果 晚于 css 10ms 之后网络再收到 js 响应,浏览器会先渲染再执行 js。

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 12, 2017

@shaopower 竟然还有这么奇怪的表现,感觉开启了新的世界,看来要尽快的去探索一下~

@jawil
Copy link

@jawil jawil commented May 12, 2017

  • CSS(外链或内联)会阻塞整个DOM的渲染(Rendering),然而DOM解析(Parsing)会正常进行
  • 很多浏览器中,CSS会延迟脚本执行和DOMContentLoaded事件
  • JS(外链或内联)会阻塞后续DOM的解析(Parsing),后续DOM的渲染(Rendering)也将被阻塞
  • JS前的DOM可以正常解析(Parsing)和渲染(Rendering)

但是,我不明白的是,既然CSS会延迟脚本的执行,我把bootstrap那个换成内联样式,就是不发送http请求下载那个css样式,好像结果又变了,这时候构建CSStree好像并没有阻塞js脚本的执行,那么,到底是HTTP线程阻塞了脚本的执行,还是构建CSStree阻塞了脚本执行?

如果是HTTP线程阻塞了js的执行,这个也可以解释,为什么多个js并行下载,要等全部下载完成才执行一个道理,我继续尝试了一下,换成img标签,这个也会发送http请求,但是这个并不会阻塞脚本的执行。

如果是构建CSStree阻塞了线程,根据上面实践的结果,发现构建CSStree并没有阻塞脚本的运行。之前看到的下面的这个说法,也就不成立。

JS 的执行有可能依赖最新样式。 比如,可能会有 var width = $('#id').width(). 这意味着,JS 代码在执行前,浏览器必须保证在此 JS 之前的所有 css(无论外链还是内嵌)都已下载和解析完成。这是 CSS 阻塞后续 JS 执行的根本原因。

那么我的猜测就是,发送的请求是是样式文件会阻塞js脚本的执行,但为什么css会阻塞js脚本文件的执行,这个我暂时也不清楚,还有浏览器预加载一些机制也不太清楚。JS 的执行有可能依赖最新样式???难道我测的结果有问题?

@jawil
Copy link

@jawil jawil commented May 12, 2017

HTML5规范要求:
脚本执行前,出现在当前<script>之前的<link rel="stylesheet">必须完全载入。
脚本执行会阻塞DOM解析。

CSS外链阻塞了脚本的执行,此时异步队列的任务该如何调度呢?我想JS引擎主线程遇到阻塞后,这时候就会放弃当前线程的代码执行,这时候JS引擎是空闲的,为了避免等待白白浪费时间,所以主线程才会读取任务队列,开始执行异步任务。等CSS外链下载完成之后console.log('second script')难道成了下一个task

还有就是别人总结的这句话,一直不太懂,这种循坏机制到底是怎么工作的,描述的也比较抽象。

同时,异步任务又可以分为两种,macrotask(宏任务)和micro(微任务)。在挂起任务时,JS 引擎会将所有任务按照类别分到这两个队列中,首先在 macrotask 的队列(这个队列也被叫做 task queue)中取出第一个任务,执行完毕后取出 microtask 队列中的所有任务顺序执行;之后再取 macrotask 任务,周而复始,直至两个队列的任务都取完。

😨😨😨,浏览器渲染解析线程调度这一块任重而道远。

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 12, 2017

嗯嗯,确实任重而道远,暂时我也没有想明白,还需要学习很多东西,非常感谢你的分析~ o( ̄▽ ̄)d

@shaopower
Copy link

@shaopower shaopower commented May 12, 2017

我觉得 js 脚本之前如果没有出现过 css 标签,那这部分脚本是不会被 css 文件阻塞的,就是说下面这块代码不会被阻塞

function printH2() {
    console.log('first script', document.querySelectorAll('h2'));
}
printH2()
setTimeout(printH2)

bootstrap.css 影响阻塞的是 second script,而且我觉得只有外链的 css 才会阻塞 js,内联的不会
在 chrome 的 timeline 或者 performance下可以看到

css 外链的时候

parse html;  
request bootstrap.css;
printH2();
setTimeout(printH2);
if (css 有缓存或者响应快){ 
    //receive css
    parse stylesheet;    
    second script;
    paint 页面;
    timeout fire;
} else {
    timeout fire;
    // receive css
    parse stylesheet;
    second script;
    paint 页面;
}

可以看到 second script 是在 parse stylesheet 之后的。

为什么有这个 if else 分支,我猜测浏览器做的某些处理,只是保证不会解析 css 后的 js 加入 event loop,也就不会执行了, 但是已经在里面的还是会执行完吧

css 改成内联后

走的逻辑类似 if css 缓存那个分支,而且没有 parse stylesheet 这一条了,parse html 也有差别,最开始就直接 parse 完了整个页面

具体为什么 css 会延迟 js 执行,我觉得这是浏览器的一种实现方式吧。

如果浏览器尚未完成 CSSOM 的下载和构建,而我们却想在此时运行脚本,会怎样?答案很简单,对性能不利:浏览器将延迟脚本执行和 DOM 构建,直至其完成 CSSOM 的下载和构建。

https://developers.google.com/web/fundamentals/performance/critical-rendering-path/adding-interactivity-with-javascript

另外并行的 js 请求也不是等全部都下载完才执行的哈,如果 script 不带 async 的话,按照在 html 里的上下位置来执行的,当然和网络也有关系。

例如下面的 2个 js 会同时请求
如果 1.js 5秒后返回,2.js 10秒后返回。在5秒的时候 1.js 会执行,10秒的时候 2.js 执行。
但是如果反过来 1.js 10秒后返回,2.js 5秒,那会在10秒的时候执行 1.js,1.js执行完开始再执行 2.js

<body>
<script src="1.js"></script>
<script src="2.js"></script>
</body>

setTimeout 是属于 macrotask,你的例子里应该和 microtask 关系不大, Promise then 和 MutationObserver 这些算 microtask

@littleluckly
Copy link

@littleluckly littleluckly commented May 12, 2017

写的很透彻,我得慢慢消化

@littleluckly
Copy link

@littleluckly littleluckly commented May 13, 2017

请教一个问题。为什么不让 Child.prototype 直接访问到 Parent.prototype ?一定要通过寄生的方式呢。。。
`
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
console.log(this.name)
}

function Child (name, age) {
Parent.call(this, name);
this.age = age;
}

// 关键的三步
//var F = function () {};

//F.prototype = Parent.prototype;

//Child.prototype = new F();
//我的修改
Child.prototype = Parent.prototype;

var child1 = new Child('kevin', '18');

console.log(child1);`

@shaopower
Copy link

@shaopower shaopower commented May 13, 2017

@littleluckly
你这样的话,增加 Child.prototype.testProp = 1; 同时会影响 Parent.prototype 的
如果不模拟,直接上 es5 的话应该是下面这样吧
Child.prototype = Object.create(Parent.prototype);

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 13, 2017

@shaopower 十分感谢回答,正解,b( ̄▽ ̄)d

@littleluckly
Copy link

@littleluckly littleluckly commented May 13, 2017

@shaopower @mqyqingfeng 谢谢解惑!

@satorioh
Copy link

@satorioh satorioh commented May 27, 2017

看了博主的文章,真是受益匪浅,新手想请教两个问题:

1.Child.prototype = Object.create(Parent.prototype);和 Child.prototype = new Parent();
这两句,除去后者会继承Parent构造函数中多余的属性/方法外,这两句代码是否是等效的?即Child.prototype最终都会指向Parent.prototype,都不破坏原型链?

2.原型上的constructor属性,除了能让子实例获得正确的父构造函数,感觉并不影响原型链继承?实际开发中是否还有其他用处?

感谢!

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 27, 2017

@satorioh Object.create 的模拟实现如下:

Object.create = function( o ) {
    function f(){}
    f.prototype = o;
    return new f;
}

使用 new Parent会带来副作用,副作用如你所说,如果抛开这一点的话,我觉得两句代码效果应该是一样的。最终 Child.prototype 的原型都是指向 Parent.prototype。

关于第二个问题,constructor确实不会影响原型链继承,constructor 用来指向对象关联的函数,我在实际开发中倒没有用过 constructor,值得注意的一点就是如果用一个新对象覆盖函数的 prototype 属性值,新对象没有 constructor 属性,实例不能通过 constructor 指向正确的构造函数。

嗯,好像没有回答什么有用的哈~

@satorioh
Copy link

@satorioh satorioh commented May 29, 2017

@mqyqingfeng 好的,还是要感谢博主的耐心解答

@lynn1824
Copy link

@lynn1824 lynn1824 commented May 31, 2017

今天一天一口气看完了博主所有的文章,分析的很透彻。希望能多写文章,多交流,共同学习进度,非常感谢楼主的分享...

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented May 31, 2017

@lynn1824 与你共勉,[]~( ̄▽ ̄)~*

@522363215
Copy link

@522363215 522363215 commented Jun 27, 2017

老铁,看完你文章,以前有好多问题都理清楚啦,看来还是要往底层深入,哈哈,谢谢博主!

@mqyqingfeng
Copy link
Owner Author

@mqyqingfeng mqyqingfeng commented Jun 27, 2017

@522363215 恭喜你看完~ []~( ̄▽ ̄)~* JavaScript 还有很多的底层知识需要探索和总结~ 与你共勉~

@zeroone001
Copy link

@zeroone001 zeroone001 commented Jun 28, 2017

🙏楼主的分享~~~谢谢,对基础再次扎实了~~~

@littleluckly
Copy link

@littleluckly littleluckly commented Aug 16, 2017

今天朋友问了我一个问题,但是我也搞不懂,所以特地来请教博主,希望给予解答。请实现函数Person,该函数实现功能如下:

var person=new Person('jim');
person.setAge(21).setPosition('developer').sayHi();
//输出‘Hello,my name is jim,21 years old, I am a developer’
@JiaJiaJiayi
Copy link

@JiaJiaJiayi JiaJiaJiayi commented Jun 30, 2020

你好,想请教一下,最后寄生组合式继承可以直接用Child5.prototype.__proto__ = Parent5.prototype;省去新创建一个函数的开销吗?
完整代码如下:

function Parent5(name) {
    this.name = name;
}

function Child5(name, age) {
    this.age = age;
    Parent5.call(this, name);
}
Parent5.prototype.getInfo = function () { return this.name + ' ' + this.age }

Child5.prototype.__proto__ = Parent5.prototype;
@YunShengTeng
Copy link

@YunShengTeng YunShengTeng commented Jul 20, 2020

你好,想请教一下,最后寄生组合式继承可以直接用Child5.prototype.__proto__ = Parent5.prototype;省去新创建一个函数的开销吗?
完整代码如下:

function Parent5(name) {
    this.name = name;
}

function Child5(name, age) {
    this.age = age;
    Parent5.call(this, name);
}
Parent5.prototype.getInfo = function () { return this.name + ' ' + this.age }

Child5.prototype.__proto__ = Parent5.prototype;

如果原型上有引用类型,那么new出的实例被共享了,不安全

@jiangzhenxiang
Copy link

@jiangzhenxiang jiangzhenxiang commented Aug 23, 2020

请问 第六种寄生组合式继承,父类上的方法sayHello为什么不会被子类继承呢,执行下面代码会报错,求教

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.sayHello = function () {
    console.log('hello');
}
Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();

var child1 = new Child('kevin', '18');

console.log(child1);
child1.getName();
child1.sayHello();    //TypeError: child1.sayHello is not a function
@inottn
Copy link

@inottn inottn commented Aug 27, 2020

请问 第六种寄生组合式继承,父类上的方法sayHello为什么不会被子类继承呢,执行下面代码会报错,求教

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.sayHello = function () {
    console.log('hello');
}
Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();

var child1 = new Child('kevin', '18');

console.log(child1);
child1.getName();
child1.sayHello();    //TypeError: child1.sayHello is not a function

@jiangzhenxiang 首先明确几个概念
父类:Parent,子类:Child, 子类的实例:child
那么你的问题其实是 父类上的静态方法 sayHello 为什么不会被子类的实例继承,这个是符合预期的,实例是不会继承类的静态方法,因为JS的继承说白了就是顺着实例的__proto__属性访问类的prototype属性,所以只会继承类的prototype上的属性。

@jiangzhenxiang
Copy link

@jiangzhenxiang jiangzhenxiang commented Aug 27, 2020

@inottn 多谢指点

@aaronlam
Copy link

@aaronlam aaronlam commented Oct 10, 2020

请问 第六种寄生组合式继承,父类上的方法sayHello为什么不会被子类继承呢,执行下面代码会报错,求教

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.sayHello = function () {
    console.log('hello');
}
Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();

var child1 = new Child('kevin', '18');

console.log(child1);
child1.getName();
child1.sayHello();    //TypeError: child1.sayHello is not a function

@jiangzhenxiang 首先明确几个概念
父类:Parent,子类:Child, 子类的实例:child
那么你的问题其实是 父类上的静态方法 sayHello 为什么不会被子类的实例继承,这个是符合预期的,实例是不会继承类的静态方法,因为JS的继承说白了就是顺着实例的__proto__属性访问类的prototype属性,所以只会继承类的prototype上的属性。

但是如果想让子类Child继承父类Parent的类静态方法sayHello ,是否可以这样?
Child.__ proto __ = Parent;
或者
Object.setPrototypeOf(Child, Parent);

感谢!

@pythonfirst
Copy link

@pythonfirst pythonfirst commented Oct 25, 2020

请问 第六种寄生组合式继承,父类上的方法sayHello为什么不会被子类继承呢,执行下面代码会报错,求教

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.sayHello = function () {
    console.log('hello');
}
Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();

var child1 = new Child('kevin', '18');

console.log(child1);
child1.getName();
child1.sayHello();    //TypeError: child1.sayHello is not a function

@jiangzhenxiang 首先明确几个概念
父类:Parent,子类:Child, 子类的实例:child
那么你的问题其实是 父类上的静态方法 sayHello 为什么不会被子类的实例继承,这个是符合预期的,实例是不会继承类的静态方法,因为JS的继承说白了就是顺着实例的__proto__属性访问类的prototype属性,所以只会继承类的prototype上的属性。

但是如果想让子类Child继承父类Parent的类静态方法sayHello ,是否可以这样?
Child.__ proto __ = Parent;
或者
Object.setPrototypeOf(Child, Parent);

感谢!

Child是构造函数,构造函数有prototype属性,实例对象具有__proto__属性,这个概念你是不是搞错了?

@aaronlam
Copy link

@aaronlam aaronlam commented Oct 25, 2020

请问 第六种寄生组合式继承,父类上的方法sayHello为什么不会被子类继承呢,执行下面代码会报错,求教

function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}
Parent.sayHello = function () {
    console.log('hello');
}
Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}
// 关键的三步
var F = function () {};

F.prototype = Parent.prototype;

Child.prototype = new F();

var child1 = new Child('kevin', '18');

console.log(child1);
child1.getName();
child1.sayHello();    //TypeError: child1.sayHello is not a function

@jiangzhenxiang 首先明确几个概念
父类:Parent,子类:Child, 子类的实例:child
那么你的问题其实是 父类上的静态方法 sayHello 为什么不会被子类的实例继承,这个是符合预期的,实例是不会继承类的静态方法,因为JS的继承说白了就是顺着实例的__proto__属性访问类的prototype属性,所以只会继承类的prototype上的属性。

但是如果想让子类Child继承父类Parent的类静态方法sayHello ,是否可以这样?
Child.__ proto __ = Parent;
或者
Object.setPrototypeOf(Child, Parent);
感谢!

Child是构造函数,构造函数有prototype属性,实例对象具有__proto__属性,这个概念你是不是搞错了?

但是我上次确定了下,貌似ES6 Module在处理类继承上貌似就是我说的那样去处理类静态方法继承关系的。。

@aaayane
Copy link

@aaayane aaayane commented Nov 4, 2020

关于第六种方式寄生组合式继承 isPrototypeOf 的问题

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。

这里实现一下

function object(o) {
    function F() { }
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

prototype(Child, Parent);
Parent.isPrototypeOf(Child) //false


请问为什么返回是false呢?

@aaronlam
Copy link

@aaronlam aaronlam commented Nov 4, 2020

关于第六种方式寄生组合式继承 isPrototypeOf 的问题

这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。

这里实现一下

function object(o) {
    function F() { }
    F.prototype = o;
    return new F();
}

function prototype(child, parent) {
    var prototype = object(parent.prototype);
    prototype.constructor = child;
    child.prototype = prototype;
}
function Parent(name) {
    this.name = name;
    this.colors = ['red', 'blue', 'green'];
}

Parent.prototype.getName = function () {
    console.log(this.name)
}

function Child(name, age) {
    Parent.call(this, name);
    this.age = age;
}

prototype(Child, Parent);
Parent.isPrototypeOf(Child) //false

请问为什么返回是false呢?

isPrototypeOf是判断原型链的,你可以这样用

const foo= Child('name', 1);

Child.prototype.isPrototypeOf(foo);
Parent.prototype.isPrototypeOf(foo);

或者这样用

Parent.prototype.isPrototypeOf(Child.prototype);
@afishhhhh
Copy link

@afishhhhh afishhhhh commented Dec 4, 2020

有个疑问希望有同学解答一下,以下这段代码来自MDN:

// 定义Person构造器
function Person(firstName) {
  this.firstName = firstName;
}

// 在Person.prototype中加入方法
Person.prototype.walk = function(){
  alert("I am walking!");
};
Person.prototype.sayHello = function(){
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器, 确保(使用Function#call)"this" 在调用过程中设置正确
  Person.call(this, firstName);

  // 初始化Student类特有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
Student.prototype = Object.create(Person.prototype); // See note below

// 设置"constructor" 属性指向Student
Student.prototype.constructor = Student;

// 更换"sayHello" 方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// 测试实例:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// Check that instanceof works correctly
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

上面的这段代码基本上就是组合式继承,在本文中以及《JavaScript高级程序设计》中,都是用Child.prototype = new Parent();来继承方法的。但是来自MDN的这段代码中却是使用Student.prototype = Object.create(Person.prototype),并且注释明确说直接new Parent()有错误,他说的错误我不理解。他说最重要的一点是我们在实例化时不能赋予Person类任何的FirstName参数,我们不是在子类的构造函数中赋予了Person类的参数吗?
正如文中提到的,Object.create的简单实现过程是这样的:

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

那么在这里也仅仅是返回的是一个prototype被赋值的实例而已,跟直接new Parent(),基本是一个意思吧。

有个疑问希望有同学解答一下,以下这段代码来自MDN:

// 定义Person构造器
function Person(firstName) {
  this.firstName = firstName;
}

// 在Person.prototype中加入方法
Person.prototype.walk = function(){
  alert("I am walking!");
};
Person.prototype.sayHello = function(){
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器, 确保(使用Function#call)"this" 在调用过程中设置正确
  Person.call(this, firstName);

  // 初始化Student类特有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
Student.prototype = Object.create(Person.prototype); // See note below

// 设置"constructor" 属性指向Student
Student.prototype.constructor = Student;

// 更换"sayHello" 方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// 测试实例:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// Check that instanceof works correctly
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

上面的这段代码基本上就是组合式继承,在本文中以及《JavaScript高级程序设计》中,都是用Child.prototype = new Parent();来继承方法的。但是来自MDN的这段代码中却是使用Student.prototype = Object.create(Person.prototype),并且注释明确说直接new Parent()有错误,他说的错误我不理解。他说最重要的一点是我们在实例化时不能赋予Person类任何的FirstName参数,我们不是在子类的构造函数中赋予了Person类的参数吗?
正如文中提到的,Object.create的简单实现过程是这样的:

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

那么在这里也仅仅是返回的是一个prototype被赋值的实例而已,跟直接new Parent(),基本是一个意思吧。

Object.create跟直接new Parent()不一样的,博主在原型链继承中也提到了直接用new Parent()会导致引用类型的属性被实例共享,Object.create就如你所说的,实现过程是创建了一个新的构造函数,在这个例子中将这个构造函数的原型指向Person.prototype,然后从这个构造函数中返回出一个实例,这个实例就是新创建的这个对象,跟直接new Parent创建的实例是有区别的,希望能对你有所帮助,有什么不对的地方还请指出

我觉得这里的问题是 new Parent() 会产生其他的副作用,而不是说引用类型的属性被共享。如果说 new Parent() 产生了引用类型的属性,比如说 friends = [...],那这个属性是存在于 Student.prototype 上的,这样的话被实例共享也没什么问题吧。而且在这里,每个实例都会有一个同名的 friends 屏蔽掉 Student.prototype 上的 friends。

@aaronlam
Copy link

@aaronlam aaronlam commented Dec 4, 2020

有个疑问希望有同学解答一下,以下这段代码来自MDN:

// 定义Person构造器
function Person(firstName) {
  this.firstName = firstName;
}

// 在Person.prototype中加入方法
Person.prototype.walk = function(){
  alert("I am walking!");
};
Person.prototype.sayHello = function(){
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器, 确保(使用Function#call)"this" 在调用过程中设置正确
  Person.call(this, firstName);

  // 初始化Student类特有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
Student.prototype = Object.create(Person.prototype); // See note below

// 设置"constructor" 属性指向Student
Student.prototype.constructor = Student;

// 更换"sayHello" 方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// 测试实例:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// Check that instanceof works correctly
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

上面的这段代码基本上就是组合式继承,在本文中以及《JavaScript高级程序设计》中,都是用Child.prototype = new Parent();来继承方法的。但是来自MDN的这段代码中却是使用Student.prototype = Object.create(Person.prototype),并且注释明确说直接new Parent()有错误,他说的错误我不理解。他说最重要的一点是我们在实例化时不能赋予Person类任何的FirstName参数,我们不是在子类的构造函数中赋予了Person类的参数吗?
正如文中提到的,Object.create的简单实现过程是这样的:

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

那么在这里也仅仅是返回的是一个prototype被赋值的实例而已,跟直接new Parent(),基本是一个意思吧。

有个疑问希望有同学解答一下,以下这段代码来自MDN:

// 定义Person构造器
function Person(firstName) {
  this.firstName = firstName;
}

// 在Person.prototype中加入方法
Person.prototype.walk = function(){
  alert("I am walking!");
};
Person.prototype.sayHello = function(){
  alert("Hello, I'm " + this.firstName);
};

// 定义Student构造器
function Student(firstName, subject) {
  // 调用父类构造器, 确保(使用Function#call)"this" 在调用过程中设置正确
  Person.call(this, firstName);

  // 初始化Student类特有属性
  this.subject = subject;
};

// 建立一个由Person.prototype继承而来的Student.prototype对象.
// 注意: 常见的错误是使用 "new Person()"来建立Student.prototype.
// 这样做的错误之处有很多, 最重要的一点是我们在实例化时
// 不能赋予Person类任何的FirstName参数
// 调用Person的正确位置如下,我们从Student中来调用它
Student.prototype = Object.create(Person.prototype); // See note below

// 设置"constructor" 属性指向Student
Student.prototype.constructor = Student;

// 更换"sayHello" 方法
Student.prototype.sayHello = function(){
  console.log("Hello, I'm " + this.firstName + ". I'm studying " + this.subject + ".");
};

// 加入"sayGoodBye" 方法
Student.prototype.sayGoodBye = function(){
  console.log("Goodbye!");
};

// 测试实例:
var student1 = new Student("Janet", "Applied Physics");
student1.sayHello();   // "Hello, I'm Janet. I'm studying Applied Physics."
student1.walk();       // "I am walking!"
student1.sayGoodBye(); // "Goodbye!"

// Check that instanceof works correctly
console.log(student1 instanceof Person);  // true 
console.log(student1 instanceof Student); // true

上面的这段代码基本上就是组合式继承,在本文中以及《JavaScript高级程序设计》中,都是用Child.prototype = new Parent();来继承方法的。但是来自MDN的这段代码中却是使用Student.prototype = Object.create(Person.prototype),并且注释明确说直接new Parent()有错误,他说的错误我不理解。他说最重要的一点是我们在实例化时不能赋予Person类任何的FirstName参数,我们不是在子类的构造函数中赋予了Person类的参数吗?
正如文中提到的,Object.create的简单实现过程是这样的:

function createObj(o) {
    function F(){}
    F.prototype = o;
    return new F();
}

那么在这里也仅仅是返回的是一个prototype被赋值的实例而已,跟直接new Parent(),基本是一个意思吧。

Object.create跟直接new Parent()不一样的,博主在原型链继承中也提到了直接用new Parent()会导致引用类型的属性被实例共享,Object.create就如你所说的,实现过程是创建了一个新的构造函数,在这个例子中将这个构造函数的原型指向Person.prototype,然后从这个构造函数中返回出一个实例,这个实例就是新创建的这个对象,跟直接new Parent创建的实例是有区别的,希望能对你有所帮助,有什么不对的地方还请指出

我觉得这里的问题是 new Parent() 会产生其他的副作用,而不是说引用类型的属性被共享。如果说 new Parent() 产生了引用类型的属性,比如说 friends = [...],那这个属性是存在于 Student.prototype 上的,这样的话被实例共享也没什么问题吧。而且在这里,每个实例都会有一个同名的 friends 屏蔽掉 Student.prototype 上的 friends。

简单的说就是 new Parent() 这个操作会把 Parent 实例对象的一些属性也设置到了 Student.prototype 里去了,但是这些实例属性是 Student.prototype 不需要并且这样设置会造成污染。所以就需要通过 Object.create(Parent.prototype) 来 create 一个纯净的 Parent.prototype 实例给到 Student.prototype

@afishhhhh
Copy link

@afishhhhh afishhhhh commented Dec 4, 2020

我觉得在原型链继承中,“引用类型的属性被所有实例共享” 这句话是有问题的。被所有实例共享的其实是整一个 Child.prototype,所以不管是引用类型还是基础类型都是被共享的。如果修改了 Child.prototype 中的某一个基础类型,也是可以反映到所有实例中的。构造函数和 prototype 应该有不同的职责,像例子中的 names,如果在这里理解为每个实例自己的状态,那就应该写在构造函数里。

第二个经典继承,本身 js 中就没有传统意义上的继承,对于这个经典继承,我觉得更称不上继承。

function Parent () {
    this.names = ['kevin', 'daisy'];
}
function Child () {
    Parent.call(this);
}
var child1 = new Child();

Parent.call(this) 用一个指定的 this 去调用 Parent,跟以下代码没有区别,而且这个 Parent 也并不会存在于原型链上。

function Child () {
    this.names = ['kevin', 'daisy'];
}
var child1 = new Child();
@cw84973570
Copy link

@cw84973570 cw84973570 commented Dec 14, 2020

我觉得在原型链继承中,“引用类型的属性被所有实例共享” 这句话是有问题的。被所有实例共享的其实是整一个 Child.prototype,所以不管是引用类型还是基础类型都是被共享的。如果修改了 Child.prototype 中的某一个基础类型,也是可以反映到所有实例中的。构造函数和 prototype 应该有不同的职责,像例子中的 names,如果在这里理解为每个实例自己的状态,那就应该写在构造函数里。

第二个经典继承,本身 js 中就没有传统意义上的继承,对于这个经典继承,我觉得更称不上继承。

function Parent () {
    this.names = ['kevin', 'daisy'];
}
function Child () {
    Parent.call(this);
}
var child1 = new Child();

Parent.call(this) 用一个指定的 this 去调用 Parent,跟以下代码没有区别,而且这个 Parent 也并不会存在于原型链上。

function Child () {
    this.names = ['kevin', 'daisy'];
}
var child1 = new Child();

“引用类型的属性被所有实例共享”,可能是因为引用类型被共享引起的问题更大,但是这样表述很容易引起误会。
原型链都没改,我也觉得把这个称之为继承略有不妥。

还有经典继承结论中关于缺点的描述:

方法都在构造函数中定义,每次创建实例都会创建一遍方法。

因为这不是真正的继承,所以我认为这里的表述也略有问题,不太好理解,应该改成“因为不会继承原型,无法通过prototype继承方法,所以只能通过构造函数定义方法,导致每次创建实例都会创建一遍方法。”

function Parent () {
    this.names = ['kevin', 'daisy'];
    this.sayNames = function () {
        console.log(this.names.join())
    }
}

最好是在例子中定义一个方法以便理解。

@songyongqin
Copy link

@songyongqin songyongqin commented Dec 16, 2020

4.原型式继承,单词
1608115007(1)
拼写错误了,及时更正一下 friends 单词

@andyyxw
Copy link

@andyyxw andyyxw commented Feb 4, 2021

请教一个问题,Child.prototype = Object.create(Parent.prototype)Child.prototype.__proto__ = Parent.prototype(或 Object.setPrototypeOf(Child.prototype, Parent.prototype)) 有什么区别吗?

@mofiggHasSugar
Copy link

@mofiggHasSugar mofiggHasSugar commented Feb 7, 2021

新手,想麻烦问个问题,求各位老师们指教。
function prototype(child,parent){
const prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
这句话prototype.constructor = child;
就是改变了构造函数Child.prototype.constructor
这个目的是什么啊?是保证了原型链吗?

@gnosis23
Copy link

@gnosis23 gnosis23 commented Feb 21, 2021

新手,想麻烦问个问题,求各位老师们指教。
function prototype(child,parent){
const prototype = object(parent.prototype);
prototype.constructor = child;
child.prototype = prototype;
}
这句话prototype.constructor = child;
就是改变了构造函数Child.prototype.constructor
这个目的是什么啊?是保证了原型链吗?

为了让新创建的实例能够找到创建它的构造函数。比如:

var instance = new Child();
// 这个时候访问 instance.constructor 就能找到 Child
@HsiaoYukun
Copy link

@HsiaoYukun HsiaoYukun commented Feb 21, 2021

请问 extends 是不是可以取代文中提到的方法了?

@cw84973570
Copy link

@cw84973570 cw84973570 commented Feb 22, 2021

请教一个问题,Child.prototype = Object.create(Parent.prototype)Child.prototype.__proto__ = Parent.prototype(或 Object.setPrototypeOf(Child.prototype, Parent.prototype)) 有什么区别吗?

第一个把整个原型都替换掉了,相当于

  function F() {}
  F.prototype = Parent.prototype
  Child.prototype = new F()

原来的原型不要了,替换成了F对象的实例,而F对象的原型又指向Parent.prototype,这样就间接和Parent产生了关系

第二个只是将Child的原型指向Parent.prototype而已,换句话说就是将Child的原型变成了Parent的一个实例,原型还是原来的原型,但是身份变成了Parent的一个实例。

第一个原型链是Child.prototype => F对象实例,F对象实例.__proto__ => Parent.prototype。
第二个原型链是Child.prototype => Child原型,Child原型.__proto__ => Parent.prototype。
说白了对象的原型同时也是另一个对象的实例。

@cjmafei
Copy link

@cjmafei cjmafei commented Mar 1, 2021

function Parent () {
    this.names = ['kevin', 'daisy'];
}

function Child () {
    Parent.call(this);
}

var child1 = new Child();

child1.names.push('yayu');

console.log(child1.names); // ["kevin", "daisy", "yayu"]

var child2 = new Child();

console.log(child2.names); // ["kevin", "daisy"]

为什么借用构造函数解决了实例共享父类属性的问题,这个问题卡了我很久,刚才突然茅塞顿开了。
Parent.call(this);改变了父类的this指向,所以不同实例间的属性是互相独立的了。

@LongYue9608
Copy link

@LongYue9608 LongYue9608 commented May 10, 2021

各位大佬请问组合继承中Child.prototype.constructor = Child; 这一步是干嘛的?如果这里加了,第一种原型链继承也可以加这一句吧

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

Successfully merging a pull request may close this issue.

None yet