原型 原型链 继承
🌸 您好,欢迎您的阅读,等君久矣,愿与君畅谈.
🔭 § 始于颜值 § 陷于才华 § 忠于人品 §
📫 希望我们可以进一步交流,共同学习,共同探索未知的技术世界 稀土掘金 OR GitHub.
# prototype
原型的概念
每一个 JavaScript 对象 (除了
null外) 创建的时候,就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型中 "继承" 属性。
在 JavaScript 中,每个函数都有一个 prototype 属性,该属性指向函数的原型对象。
# __ proto __
每个对象 (除了 null 外) 都会有的属性,叫做 __proto__ ,这个属性会指向该对象的原型。
绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于
Person.prototype中,实际上,它是来自于Object.prototype,与其说是一个属性,不如说是一个getter/setter,当使用obj.__proto__时,可以理解成返回了Object.getPrototypeOf(obj)
# constructor
每个原型都有一个 constructor ,指向该关联的构造函数
当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到 constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取
# 实例与原型
当读取实例的属性时,如果找不到,就通过隐式原型 ( __proto__ ) 向上查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层,若最顶层也找不到,则返回 undefined。
# 原型的原型
1 | |
原型对象就是通过 Object 构造函数生成的。又因为实例的 __proto__ 指向构造函数的 prototype 所以得到总的关系图:

# 原型链
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。
—— 摘自《javascript 高级程序设计》
最后,由于 Object 的原型对象是 null,所以得出最终的图

# 继承
# 原型链继承
1 | |
# 优点:
- 父类方法可以复用
# 缺点:
- 父类的引用属性会被所有子类实例共享,多个实例对引用类型的操作会被篡改
- 子类构建实例时不能向父类传递参数
# 构造函数继承
使用父类构造函数来增强子类实例,等同于复制父类的实例给子类 (不使用原型)
1 | |
创建子类实例时调用 SuperType 构造函数,于是 SubType 的每个实例都会将 SuperType 中的属性复制一份,解决了原型链继承中多实例相互影响的问题。
# 优点:
- 父类的引用属性不会被共享
- 子类构建实例时可以向父类传递参数
# 缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性 / 方法
- 无法实现复用,每个子类都有父类实例函数的副本,影响性能
# 组合继承(上面两种结合起来)
组合上述两种方法,用原型链实现对原型属性的继承,用构造函数来实现实例属性的继承
1 | |
# 优点:
- 父类的方法可以被复用
- 父类的引用属性不会被共享
- 子类构建实例时可以向父类传递参数
# 缺点(对照注释):
- 第一次调用
SuperType():给SubType.prototype写入两个属性 name,color。 - 第二次调用
SuperType():给instance1写入两个属性 name,color。
实例对象 inst1 上的两个属性就屏蔽了其原型对象 SubType.prototype 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的父类实例的属性 / 方法。这种被覆盖的情况造成了性能上的浪费。
# 原型式继承 (浅拷贝)
Object.create()
1 | |
# 优点:
- 父类方法可以复用
# 缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
- 子类构建实例时不能向父类传递参数
# 寄生式继承(能附加一些方法)
使用原型式继承获得一份目标对象的浅拷贝,然后增强了这个浅拷贝的能力。
优缺点其实和原型式继承一样,寄生式继承说白了就是能在拷贝来的对象上加点方法,也就是所谓增强能力。
1 | |
# 优点:
- 父类方法可以复用
# 缺点:
- 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
- 子类构建实例时不能向父类传递参数
# 寄生组合继承(最优方案)
1 | |
# 多继承
如果你希望能继承到多个对象,则可以使用混入的方式。
1 | |
Object.assign 会把
OtherSuperClass原型上的函数拷贝到MyClass原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。Object.assign 是在 ES2015 引入的,且可用 polyfilled。要支持旧浏览器的话,可用使用 jQuery.extend () 或者_.assign ()。 ——[MDN] Object.create ()
# ES6 extends
虽然 ES6 引入了关键字 class,但是底层仍然是基于原型的实现。class 只是语法糖,使得在 JavaScript 模拟类的代码更为简洁。
——《JavaScript 忍者秘籍》
1 | |
super 实现的原理
就是将继承的那个父类对象在子类中调用,比如 super.call(this) 实现将父类中的属性 (父类的方法是通过原型链来继承,实例都可以共享这些方法) 在子类中声明。
# 作用域和作用域链
# 作用域的概念
字面意思就是起作用的范围。
# 全局作用域
在代码中任何地方都能访问到的对象拥有全局作用域
常见情况
- 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
- 所有末定义直接赋值的变量自动声明为拥有全局作用域
- 所有 window 对象的属性拥有全局作用域
- 此处的 window 对象意味顶层对象,不同环境下有可能顶层对象不同
# 局部作用域
块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
# 暂时性死区
- var 的创建和初始化被提升,赋值不会被提升。
- let 的创建被提升,初始化和赋值不会被提升。
- function 的创建、初始化和赋值均会被提升。
1 | |
# 函数作用域
指在函数内部生效。
# 作用域链
当前作用域内找不到的变量会根据作用域链向上寻找,直到顶层对象 window 也没有就返回 undefined。
# 作用域与执行上下文
JavaScript 属于解释型语言,JavaScript 的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:
# 解释阶段:
- 词法分析
- 语法分析
- 作用域规则确定
# 执行阶段:
- 创建执行上下文
- 执行函数代码
- 垃圾回收
JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是 this 的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。
一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值
