原型 原型链 继承

🌸 您好,欢迎您的阅读,等君久矣,愿与君畅谈.
🔭 § 始于颜值 § 陷于才华 § 忠于人品 §
📫 希望我们可以进一步交流,共同学习,共同探索未知的技术世界 稀土掘金 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
2
3
4
//原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它
var obj = new Object();
obj.name = 'Kevin';
console.log(obj.name);

原型对象就是通过 Object 构造函数生成的。又因为实例的 __proto__ 指向构造函数的 prototype 所以得到总的关系图:

img

# 原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。这就是所谓的原型链的基本概念。

​ —— 摘自《javascript 高级程序设计》

最后,由于 Object 的原型对象是 null,所以得出最终的图

img

img

# 继承

# 原型链继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function SuperType() {0
this.prototype = true;
}
SuperType.prototype.getSuperValue = function() {
return this.prototype
}
function SubType() {
this.subProperty = false
}
SubType.prototype.getSubValue = function() {
return this.subProperty
}
// 关键,创建SuperType的实例,让SubType.prototype指向这个实例
SubType.prototype = new SuperType()
console.dir(SuperType)
let inst1 = new SuperType()
let inst2 = new SubType()
console.log(inst2.getSuperValue()) // true
# 优点:
  • 父类方法可以复用
# 缺点:
  • 父类的引用属性会被所有子类实例共享,多个实例对引用类型的操作会被篡改
  • 子类构建实例时不能向父类传递参数
# 构造函数继承

使用父类构造函数来增强子类实例,等同于复制父类的实例给子类 (不使用原型)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function SuperType() {
this.color = ['red', 'green']
}

// 构造函数继承
// 使得每个实例都会复制得到自己独有的一份属性
function SubType() {
// 将父对象的构造函数绑定在子对象上
SuperType.call(this)
}

let inst1 = new SubType()

console.log(inst1)

// SubType {color: Array(2)}

创建子类实例时调用 SuperType 构造函数,于是 SubType 的每个实例都会将 SuperType 中的属性复制一份,解决了原型链继承中多实例相互影响的问题

# 优点:
  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数
# 缺点:
  • 只能继承父类的实例属性和方法,不能继承原型属性 / 方法
  • 无法实现复用,每个子类都有父类实例函数的副本,影响性能
# 组合继承(上面两种结合起来)

组合上述两种方法,用原型链实现对原型属性的继承,用构造函数来实现实例属性的继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function SuperType(name) {
this.name = name
this.colors = ['red', 'blue', 'green']
}

SuperType.prototype.getName = function () {
return this.name
}

function SubType(name, age) {
// 1、构造函数来复制父类的属性给SubType实例
// *** 第二次调用SuperType()
SuperType.call(this, name)
// call() 允许为不同的对象分配和调用属于一个对象的函数/方法。
// call() 提供新的 this 值给当前调用的函数/方法。你可以使用 call 来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
this.age = age
}

SubType.prototype.getAge = function () {
return this.age
}

// 2、原型继承
// *** 第一次调用SuperType()
SubType.prototype = new SuperType()
// 手动挂上构造器,指向自己的构造函数 SubType
SubType.prototype.constructor = SubType
SubType.prototype.getAge = function () {
return this.age
}

let inst1 = new SubType('Asuna', 20)

console.log('inst1', inst1)
console.log(inst1.getName(), inst1.getAge())
console.log(inst1 instanceof SubType, inst1 instanceof SuperType)


// inst1 SubType {name: "Asuna", colors: Array(3), age: 20}
// Asuna 20
// true true
# 优点:
  • 父类的方法可以被复用
  • 父类的引用属性不会被共享
  • 子类构建实例时可以向父类传递参数
# 缺点(对照注释):
  • 第一次调用 SuperType() :给 SubType.prototype 写入两个属性 name,color。
  • 第二次调用 SuperType() :给 instance1 写入两个属性 name,color。

实例对象 inst1 上的两个属性就屏蔽了其原型对象 SubType.prototype 的两个同名属性。所以,组合模式的缺点就是在使用子类创建实例对象时,其原型中会存在两份相同的父类实例的属性 / 方法。这种被覆盖的情况造成了性能上的浪费。

# 原型式继承 (浅拷贝)

Object.create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};

let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");

let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");

alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
# 优点:
  • 父类方法可以复用
# 缺点:
  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
  • 子类构建实例时不能向父类传递参数
# 寄生式继承(能附加一些方法)

使用原型式继承获得一份目标对象的浅拷贝,然后增强了这个浅拷贝的能力。

优缺点其实和原型式继承一样,寄生式继承说白了就是能在拷贝来的对象上加点方法,也就是所谓增强能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function object(obj) {
function F() { }
F.prototype = obj
return new F()
}

function createAnother(original) {
// 通过调用函数创建一个新对象
let clone = object(original)
//以某种方式来增强这个对象
clone.getName = function () {
console.log('我有了getName方法: ' + this.name)
}
return clone
}

let person = {
name: 'Asuna',
friends: ['Kirito', 'Yuuki', 'Sinon']
}

let inst1 = createAnother(person)
let inst2 = createAnother(person)
# 优点:
  • 父类方法可以复用
# 缺点:
  • 原型链继承多个实例的引用类型属性指向相同,存在篡改的可能
  • 子类构建实例时不能向父类传递参数
# 寄生组合继承(最优方案)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function inheritPrototype(subType, superType) {
// 修正子类原型对象指针,指向父类原型的一个副本 (用object()也可以)
subType.prototype = Object.create(superType.prototype)
// 增强对象,弥补因重写原型而失去的默认的constructor属性
subType.prototype.constructor = subType
}

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

SuperType.prototype.getColors = function () {
console.log(this.colors)
}

function SubType(name, age) {
SuperType.call(this, name)
this.age = age
}

inheritPrototype(SubType, SuperType)

SubType.prototype.getAge = function () {
console.log(this.age)
}

let inst1 = new SubType("Asuna", 20)
let inst2 = new SubType("Krito", 21)
console.log('inst1', inst1)
console.log('inst2', inst2)
# 多继承

如果你希望能继承到多个对象,则可以使用混入的方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}

// 继承一个类(就是寄生组合继承的套路)
MyClass.prototype = Object.create(SuperClass.prototype);

// 混合其它类,关键是这里的 assign() 方法
Object.assign(MyClass.prototype, OtherSuperClass.prototype);

// 重新指定constructor
MyClass.prototype.constructor = MyClass;

// 在之类上附加方法
MyClass.prototype.myMethod = function() {
// do a thing
};

Object.assign 会把 OtherSuperClass 原型上的函数拷贝到 MyClass 原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。Object.assign 是在 ES2015 引入的,且可用 polyfilled。要支持旧浏览器的话,可用使用 jQuery.extend () 或者_.assign ()。 ——[MDN] Object.create ()

# ES6 extends

虽然 ES6 引入了关键字 class,但是底层仍然是基于原型的实现。class 只是语法糖,使得在 JavaScript 模拟类的代码更为简洁。

​ ——《JavaScript 忍者秘籍》

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Person {
constructor(name) {
this.name = name
}

// 原型方法
// 即 Person.prototype.getName = function() { }
// 下面可以简写为 getName() {...}
getName = function () {
console.log('Person:', this.name)
}
}

class Gamer extends Person {
constructor(name, age) {
// 子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
super(name)
this.age = age
}
}

const asuna = new Gamer('Asuna', 20)
asuna.getName() // 成功访问到父类的方法

super 实现的原理

就是将继承的那个父类对象在子类中调用,比如 super.call(this) 实现将父类中的属性 (父类的方法是通过原型链来继承,实例都可以共享这些方法) 在子类中声明。

# 作用域和作用域链

# 作用域的概念

字面意思就是起作用的范围。

# 全局作用域

在代码中任何地方都能访问到的对象拥有全局作用域

常见情况

  • 最外层函数 和在最外层函数外面定义的变量拥有全局作用域
  • 所有末定义直接赋值的变量自动声明为拥有全局作用域
  • 所有 window 对象的属性拥有全局作用域
    • 此处的 window 对象意味顶层对象,不同环境下有可能顶层对象不同

# 局部作用域

块级作用域可通过新增命令 let 和 const 声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块(由一对花括号包裹)内部
# 暂时性死区
  • var 的创建和初始化被提升,赋值不会被提升。
  • let 的创建被提升,初始化和赋值不会被提升。
  • function 的创建、初始化和赋值均会被提升。
1
2
3
4
5
function test(){
console.log(a)
let a = 7;
}
test()
# 函数作用域

指在函数内部生效。

# 作用域链

当前作用域内找不到的变量会根据作用域链向上寻找,直到顶层对象 window 也没有就返回 undefined。

# 作用域与执行上下文

JavaScript 属于解释型语言,JavaScript 的执行分为:解释和执行两个阶段,这两个阶段所做的事并不一样:

# 解释阶段:
  • 词法分析
  • 语法分析
  • 作用域规则确定
# 执行阶段:
  • 创建执行上下文
  • 执行函数代码
  • 垃圾回收

JavaScript 解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,但是执行上下文是函数执行之前创建的。执行上下文最明显的就是 this 的指向是执行时确定的。而作用域访问的变量是编写代码的结构确定的。

一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值


原型 原型链 继承
http://example.com/2022/06/19/4003_JavaScript原型、原型链、继承、作用域、作用域链/
作者
XGG
发布于
2022年6月19日
更新于
2023年6月3日
许可协议