在理解继承之前,需要知道 js 的三个东西:

  1. 什么是 JS 原型链

  2. this 的值到底是什么

  3. JS 的new 到底是干什么的

一、什么是 JS 原型链?

我们知道 JS 有对象,比如

var obj = { name: 'obj' }

我们通过控制台把obj 打印出来:

我们会发现 obj已经有几个属性(方法)了。那么问题来了: valueOf/toString/constructor 是怎么来?我们并没有给 obj.valueOf 赋值呀。

上面这个图有点难懂,手画一个示意图:

我们发现控制台打出来的结果是:

  1. obj本身有一个属性 name (这是我们给它加的)

  2. obj还有一个属性叫做 proto(它是一个对象)

  3. obj还有一个属性,包括 valueOf, toString, constructor等

  4. obj.proto其实也有一个叫做proto的属性(console.log没有显示),值为 null

现在回到我们的问题:obj 为什么会拥有 valueOf / toString / constructor 这几个属性?

答案: 这跟 _proto_有关 。

当我们「读取」 obj.toString 时,JS 引擎会做下面的事情:

  1. 看看 obj 对象本身有没有 toString 属性。没有就走到下一步。

  2. 看看 obj.__proto__ 对象有没有 toString 属性, 发现 obj.__proto__toString 属性, 于是找到了,所以 obj.toString实际就是第2步中找到的 obj.__proto__.toString

  3. 如果 obj.__proto__没有,那么浏览器会继续查看 obj.__proto__.__proto__

  4. 如果 obj.__proto__.__proto__ 也没有,那么浏览器会继续查看 obj.__proto__.__proto__.__proto__

5.直到找到 toString 或者 __proto__null

上面的过程,就是「读」属性的「搜索过程」。而这个「搜索过程」,是连着由 proto组成的链子一直走的。这个链子,就叫做「原型链」。

共享原型链

现在我们还有另一个对象

var obj2 = { name: 'obj2' }

如图:

那么 obj.toStringobj2.toString 其实是同一东西, 也就是 obj2.__proto__.toString。 说白了,我们改其中的一个 __proto__.toString ,那么另外一个其实也会变!

差异化

如果我们想让 obj.toString 和 obj2.toString 的行为不同怎么做呢? 直接赋值就好了:

obj.toString = function(){ return '新的 toString 方法' }

小结

  1. [读]属性时会沿着原型链搜索

  2. [新增]属性时不会去看原型链

二、 this 的值到底是什么

你可能遇到过这样的 JS 面试题:

var obj = {

foo: function(){

console.log(this)

}

}

var bar = obj.foo

obj.foo() // 打印出的 this 是 obj

bar() // 打印出的 this 是 window

请解释最后两行函数的值为什么不一样。

函数调用

JS(ES5)里面有三种函数调用形式:

func(p1, p2)

obj.child.method(p1, p2)

func.call(context, p1, p2) // 先不讲 apply

一般,初学者都知道前两种形式,而且认为前两种形式「优于」第三种形式。

我们方方老师大姥说了,你一定要记住,第三种调用形式,才是正常调用形式:

func.call(context, p1, p2)

其他两种都是语法糖,可以等价地变为 call 形式:

func(p1, p2)等价于 func.call(undefined, p1, p2);

obj.child.method(p1, p2) 等价于 obj.child.method.call(obj.child, p1, p2);

至此我们的函数调用只有一种形式:

func.call(context, p1, p2)

这样,this 就好解释了 this就是上面 context

this 是你 call 一个函数时传的 context,由于你从来不用 call 形式的函数调用,所以你一直不知道。

先看 func(p1,p2) 中的 this 如何确定:

当你写下面代码时

function func(){

console.log(this)

}

func()

等价于

function func(){

console.log(this)

}

func.call(undefined) // 可以简写为 func.call()

按理说打印出来的 this 应该就是 undefined 了吧,但是浏览器里有一条规则:

如果你传的 contextnull 或者 undefined,那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)

因此上面的打印结果是 window。如果你希望这里的 this 不是 window,很简单:

func.call(obj) // 那么里面的 this 就是 obj 对象了

回到题目:

var obj = {

foo: function(){

console.log(this)

}

}

var bar = obj.foo

obj.foo() // 转换为 obj.foo.call(obj),this 就是 obj

bar()

// 转换为 bar.call()

// 由于没有传 context

// 所以 this 就是 undefined

// 最后浏览器给你一个默认的 this —— window 对象

[ ] 语法

function fn (){ console.log(this) }

var arr = [fn, fn2]

arr[0]() // 这里面的 this 又是什么呢?

我们可以把 arr[0]() 想象为 arr.0(),虽然后者的语法错了,但是形式与转换代码里的 obj.child.method(p1,p2) 对应上了,于是就可以愉快的转换了:

arr[0]()

假想为 arr.0()

然后转换为 arr.0.call(arr)

那么里面的 this 就是 arr 了

小结:

  1. this 就是你 call 一个函数时,传入的第一个参数。

  2. 如果你的函数调用不是 call 形式, 请将其转换为 call 形式

三、JS 的 new 到底是干什么的?

我们声明一个士兵,具有如下属性:

var 士兵 = {

ID: 1, // 用于区分每个士兵

兵种:"美国大兵",

攻击力:5,

生命值:42,

行走:function(){ /*走俩步的代码*/},

奔跑:function(){ /*狂奔的代码*/ },

死亡:function(){ /*Go die*/ },

攻击:function(){ /*糊他熊脸*/ },

防御:function(){ /*护脸*/ }

}

我们制造一个士兵, 只需要这样:

兵营.制造(士兵)

如果需要制造 100 个士兵怎么办呢?

循环 100 次吧:

var 士兵们 = []

var 士兵

for(var i=0; i<100; i++){

士兵 = {

ID: i, // ID 不能重复

兵种:"美国大兵",

攻击力:5,

生命值:42,

行走:function(){ /*走俩步的代码*/},

奔跑:function(){ /*狂奔的代码*/ },

死亡:function(){ /*Go die*/ },

攻击:function(){ /*糊他熊脸*/ },

防御:function(){ /*护脸*/ }

}

士兵们.push(士兵)

}

兵营.批量制造(士兵们)

哎呀,看起来好简单

质疑

上面的代码存在一个问题:浪费了很多内存

  1. 行走、奔跑、死亡、攻击、防御这五个动作对于每个士兵其实是一样的,只需要各自引用同一个函数就可以了,没必要重复创建 100 个行走、100个奔跑……

  2. 这些士兵的兵种和攻击力都是一样的,没必要创建 100 次。

  3. 只有 ID 和生命值需要创建 100 次,因为每个士兵有自己的 ID 和生命值。

改进

通过第一节可以知道 ,我们可以通过原型链来解决重复创建的问题:我们先创建一个「士兵原型」,然后让「士兵」的 proto 指向「士兵原型」。

var 士兵原型 = {

兵种:"美国大兵",

攻击力:5,

行走:function(){ /*走俩步的代码*/},

奔跑:function(){ /*狂奔的代码*/ },

死亡:function(){ /*Go die*/ },

攻击:function(){ /*糊他熊脸*/ },

防御:function(){ /*护脸*/ }

}

var 士兵们 = []

var 士兵

for(var i=0; i<100; i++){

士兵 = {

ID: i, // ID 不能重复

生命值:42

}

/*实际工作中不要这样写,因为 __proto__ 不是标准属性*/

士兵.__proto__ = 士兵原型

士兵们.push(士兵)

}

兵营.批量制造(士兵们)

优雅?

有人指出创建一个士兵的代码分散在两个地方很不优雅,于是我们用一个函数把这两部分联系起来:

function 士兵(ID){

var 临时对象 = {};

临时对象.__proto__ = 士兵.原型;

临时对象.ID = ID;

临时对象.生命值 = 42;

return 临时对象;

}

士兵.原型 = {

兵种:"美国大兵",

攻击力:5,

行走:function(){ /*走俩步的代码*/},

奔跑:function(){ /*狂奔的代码*/ },

死亡:function(){ /*Go die*/ },

攻击:function(){ /*糊他熊脸*/ },

防御:function(){ /*护脸*/ }

}

// 保存为文件:士兵.js

然后就可以愉快地引用「士兵」来创建士兵了:

var 士兵们 = []

for(var i=0; i<100; i++){

士兵们.push(士兵(i))

}

兵营.批量制造(士兵们)

JS 之父看到大家都这么搞,觉得何必呢,我给你们个糖吃,于是 JS 之父创建了 new 关键字,可以让我们少写几行代码:

只要你在士兵前面使用 new 关键字,那么可以少做四件事情:

  1. 不用创建临时对象,因为 new 会帮你做(你使用「this」就可以访问到临时对象);

  2. 不用绑定原型,因为 new 会帮你做(new 为了知道原型在哪,所以指定原型的名字 prototype);

  3. 不用 return 临时对象,因为 new 会帮你做;

  4. 不要给原型想名字了,因为 new 指定名字为 prototype

这一次用 new 来写

function 士兵(ID){

this.ID = ID

this.生命值 = 42

}

士兵.prototype = {

兵种:"美国大兵",

攻击力:5,

行走:function(){ /*走俩步的代码*/},

奔跑:function(){ /*狂奔的代码*/ },

死亡:function(){ /*Go die*/ },

攻击:function(){ /*糊他熊脸*/ },

防御:function(){ /*护脸*/ }

}

// 保存为文件:士兵.js

然后是创建士兵(加了一个 new 关键字):

var 士兵们 = []

for(var i=0; i<100; i++){

士兵们.push(new 士兵(i))

}

兵营.批量制造(士兵们)

new 的作用,就是省那么几行代码。(也就是所谓的语法糖)

注意 constructor 属性

new 操作为了记录「临时对象是由哪个函数创建的」,所以预先给「士兵.prototype」加了一个 constructor 属性:

士兵.prototype = {

constructor: 士兵

}

如果你重新对「士兵.prototype」赋值,那么这个 constructor 属性就没了,所以你应该这么写:

士兵.prototype.兵种 = "美国大兵"

士兵.prototype.攻击力 = 5

士兵.prototype.行走 = function(){ /*走俩步的代码*/}

士兵.prototype.奔跑 = function(){ /*狂奔的代码*/ }

士兵.prototype.死亡 = function(){ /*Go die*/ }

士兵.prototype.攻击 = function(){ /*糊他熊脸*/ }

士兵.prototype.防御 = function(){ /*护脸*/ }

或者你也可以自己给 constructor 重新赋值:

士兵.prototype = {

constructor: 士兵,

兵种:"美国大兵",

攻击力:5,

行走:function(){ /*走俩步的代码*/},

奔跑:function(){ /*狂奔的代码*/ },

死亡:function(){ /*Go die*/ },

攻击:function(){ /*糊他熊脸*/ },

防御:function(){ /*护脸*/ }

}

四、继承

继承的本质就是上面的讲的原型链

1)借助构造函数实现继承

function Parent1() {

this.name = 'parent1';

}

Parent1.prototype.say = function () {}

function Child1() {

Parent1.call(this);

this.type = 'child';

}

console.log(new Child1);

打印结果:

这个主要是借用call 来改变this的指向,通过 call 调用 Parent ,此时 Parent 中的 this是指 Child1。有个缺点,从打印结果看出 Child1并没有 say方法,所以这种只能继承父类的实例属性和方法,不能继承原型属性/方法。

2)借助原型链实现继承

/**

* 借助原型链实现继承

*/

function Parent2() {

this.name = 'parent2';

this.play = [1, 2, 3];

}

function Child2() {

this.type = 'child2';

}

Child2.prototype = new Parent2();

console.log(new Child2);

var s1 = new Child2();

var s2 = new Child2();

打印:

通过一讲的,我们知道要共享莫些属性,需要 对象.__proto__=父亲对象的.prototype,但实际上我们是不能直接 操作 __proto__,这时我们可以借用 new 来做,所以Child2.prototype=newParent2();<=>Child2.prototype.__proto__=Parent2.prototype; 这样我们借助 new 这个语法糖,就可以实现原型链继承。但这里有个总是,如打印结果,我们给 s1.play新增一个值 , s2 也跟着改了。所以这个是原型链继承的缺点,原因是 s1.__pro__和s2.__pro__指向同一个地址即 父类的 prototype

3)组合方式实现继承

/**

* 组合方式

*/

function Parent3() {

this.name = 'parent3';

this.play = [1, 2, 3];

}

Parent3.prototype.say = function () { }

function Child3 () {

Parent3.call(this);

this.type = 'child3';

}

Child3.prototype = new Parent3();

var s3 = new Child3();

var s4 = new Child3();

s3.play.push(4);

console.log(new Child3);

console.log(s3.play, s4.play)

打印:

将 1 和 2 两种方式组合起来,就可以解决1和2存在问题,这种方式为组合继承。这种方式有点缺点就是我实例一个对象的时, 父类 new 了两次,一次是var s3 = new Child3()对应 Child3.prototype = new Parent3()还要new 一次。

4)组合继承的优化1

function Parent4() {

this.name = 'parent4';

this.play = [1, 2, 3];

}

Parent4.prototype.say = function () { }

function Child4() {

Parent4.call(this);

this.type = 'child4';

}

Child4.prototype = Parent4.prototype;

var s5 = new Child4();

var s6 = new Child4();

这边主要为 Child4.prototype = Parent4.prototype, 因为我们通过构造函数就可以拿到所有属性和实例的方法,那么现在我想继承父类的原型对象,所以你直接赋值给我就行,不用在去 new 一次父类。其实这种方法还是有问题的,如果我在控制台打印以下两句:

从打印可以看出,此时我是没有办法区分一个对象 是直接 由它的子类实例化还是父类呢?我们还有一个方法判断来判断对象是否是类的实例,那就是用 constructor,我在控制台打印以下内容:

咦,你会发现它指向的是父类 ,这显然不是我们想要的结果, 上面讲过我们 prototype里面有一个 constructor, 而我们此时子类的 prototype 指向是 父类的 prototye ,而父类prototype里面的contructor当然是父类自己的,这个就是产生该问题的原因。

5)组合继承的优化2

/**

* 组合继承的优化2

*/

function Parent5() {

this.name = 'parent4';

this.play = [1, 2, 3];

}

Parent5.prototype.say = function () { }

function Child5() {

Parent5.call(this);

this.type = 'child4';

}

Child5.prototype = Object.create(Parent5.prototype);

这里主要使用Object.create(),它的作用是将对象继承到proto属性上。举个例子:

var test = Object.create({x:123,y:345});

console.log(test);//{}

console.log(test.x);//123

console.log(test.__proto__.x);//3

console.log(test.__proto__.x === test.x);//true

那大家可能说这样解决了吗,其实没有解决,因为这时 Child5.prototype 还是没有自己的 constructor,它要找的话还是向自己的原型对象上找最后还是找到 Parent5.prototype, constructor还是 Parent5 ,所以要给 Child5.prototype 写自己的 constructor:

Child5.prototype = Object.create(Parent5.prototype);

Child5.prototype.constructor = Child5;

参考

什么是 JS 原型链? this 的值到底是什么?一次说清楚 JS 的 new 到底是干什么的?

你的点赞是我持续分享好东西的动力,欢迎点赞!

proto文件支持继承吗_搞懂 Javascript中this 指向及继承原理相关推荐

  1. 彻底搞懂javascript中的replace函数

    javascript这门语言一直就像一位带着面纱的美女,总是看不清,摸不透,一直专注服务器端,也从来没有特别重视过,直到最近几年,javascript越来越重要,越来越通用.最近和前端走的比较近,借此 ...

  2. 来一轮带注释的demo,彻底搞懂javascript中的replace函数

    javascript这门语言一直就像一位带着面纱的美女,总是看不清,摸不透,一直专注服务器端,也从来没有特别重视过,直到最近几年,javascript越来越重要,越来越通用.最近和前端走的比较近,借此 ...

  3. java 自旋锁_搞懂Java中的自旋锁

    轻松搞懂Java中的自旋锁 前言 在之前的文章<一文彻底搞懂面试中常问的各种"锁">中介绍了Java中的各种"锁",可能对于不是很了解这些概念的同学 ...

  4. $.ligerdialog.open中确定按钮加事件_彻底搞懂JavaScript中的this指向问题

    JavaScript中的this是让很多开发者头疼的地方,而this关键字又是一个非常重要的语法点.毫不夸张地说,不理解它的含义,大部分开发任务都无法完成. 想要理解this,你可以先记住以下两点: ...

  5. 搞懂JavaScript中的进制与进制转换

    文章目录 进制介绍 进制转换 parseInt(str, radix) Number() +(一元运算符) Number.prototype.toString(radix) 自定义转换 十进制与十六进 ...

  6. python怎么安装re模块_搞懂python中的re模块

    现在介绍如何在Python中使用正则表达式. Python提供了re模块,用于实现正则表达式的操作. 通过使用re模块的方法进行字符串处理 re模块提供的方法(如search().match().fi ...

  7. 针对还没搞懂javascript中this关键字的同学

    本篇文章主要针对搞不清this指向的的同学们!不定期更新文章都是我学习过程中积累下的经验,还请大家多多关注我的文章以帮助更多的同学,不对的地方还望留言支持改进! 首先,必须搞清楚在JS里面,函数的几种 ...

  8. 彻底搞懂javascript中的match, exec的区别

    在工作中经常发现一些同学把这两个方法搞混,以致把自己弄的很郁闷.所以我和大家一起来探讨一下这两个方法的奥妙之处吧. 我们分以下几点来讲解: 相同点: 1.两个方法都是查找符合条件的匹配项,并以数组形式 ...

  9. 帮你彻底搞懂JS中的prototype、__proto__与constructor(图解)

    帮你彻底搞懂JS中的prototype.__proto__与constructor(图解) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文 ...

最新文章

  1. 如何在指定文件夹下进入jupyter notebook
  2. C语言中的typedef
  3. http://download.eclipse.org/technology/m2e/releases install error
  4. Docker容器相关命令
  5. [分布式]事务处理的常见方法
  6. CodeForces 451A
  7. myeclipse问题
  8. FileFilter接口 java
  9. golang语言中bytes包的常用函数,Reader和Buffer的使用
  10. 服务器性能发挥,浪潮服务器发挥性能优势,算力“焦虑”问题被解决
  11. 金蝶移动bos开发教程_求助临沂金蝶k3,kis,eas软件各版本优势
  12. mybaits 返回ListString
  13. SQL数据库打包发送与接收
  14. 数学中的皇冠——数论
  15. 项目管理 之一 软件开发生命周期(软件开发过程、瀑布模型、敏捷开发等)
  16. EDEM快速填充的方法
  17. 802.15.4协议简介
  18. 苹果 微信发件 服务器,如何使用iPhone自带的邮件客户端管理企业邮箱?
  19. 说说12306,呆在深圳就只能一直抢票
  20. linux服务器下mysql完全卸载

热门文章

  1. django 自定义日志配置
  2. Android中View绘制流程以及invalidate()等相关方法分析
  3. 10月第1周中国.COM域名增1万个 涨幅环比缩小82%
  4. 如何找回误删并清除了回收站的文档
  5. Django中html里的分页显示
  6. 数组的遍历你都会用了,那Promise版本的呢
  7. C++中operator关键字(重载操作符)
  8. NHibernate之Mapping 之 Property
  9. Mysql Select 语句中实现的判断
  10. PC厂商如何演化移动互联网市场格局?