前端学习过程中,有一个头痛的问题就是:散点式学习遗忘率偏高。而一名优秀的开发者应当具备穿点成线、聚线为网的信息整理能力。分清各个知识点的君臣佐使,使其相互支撑印证。
本文旨在以更合理的顺序梳理 JavaScript 核心知识点,帮助读者巩固复习 JS 核心知识:
一、执行 JavaScript 代码
在学 JS 核心概念之前,我们首先要搞明白一个事情:JavaScript 代码是在哪里执行的?
JS 代码是在浏览器、Node、CPU 里执行吗?
答:是的,但不够准确。此三者虽然提供了程序执行提供了容器与必要的 API,但它们本身并不直接执行 JavaScript 代码。JS 代码是由 JavaScript引擎 解析和执行的, 它负责将开发者编写的 JavaScript 代码转换为可由计算机执行的低级机器代码。
JavaScript 引擎的作用是解析、编译、执行 JavaScript 代码,管理内存,并提供运行时环境。
JS引擎主要职责
- 解析代码:JavaScript 引擎首先会解析 JavaScript 代码,将其转换为抽象语法树(AST)。
- 编译和优化:然后,引擎将 AST 转换为字节码或直接转换为机器代码。一些现代的 JavaScript 引擎(如 V8)使用即时编译(JIT)技术,它们在解析代码的同时进行编译,以提高性能。
- 执行代码:引擎执行转换后的代码。在这个过程中,它会管理内存,创建变量和函数,并执行它们。
- 垃圾回收:JavaScript 引擎还负责内存管理和垃圾回收。当对象不再被引用或不再需要时,引擎会自动释放其占用的内存。
- 提供运行时环境:JavaScript 引擎还提供了运行时环境,包括执行上下文栈、作用域链、事件队列等。
为了转换和执行 JavaScript 代码,JS 引擎会创建一个环境称为执行上下文,包含了所有必要的状态信息,以便代码能够正确地运行。当你运行一个 JS 脚本或调用一个函数时:一个新的「执行上下文」就会被创建,并压入「执行上下文栈」中。至此,引出两个概念:
1. 执行上下文(Execution Context)
一句话说明:执行上下文是 JavaScript 代码运行时的环境,它包含了变量、函数和 this 。
选择开篇介绍上下文,是因为它是学习作用域、闭包的前置知识
它可以分为两类:全局 / 函数执行上下文
- 全局执行上下文(GEC) :它是 JavaScript 代码加载时,首先创建的上下文。所有不在函数内部的代码,都在GEC里执行。它只做两件事:
- 创建一个全局变量对象,在浏览器中这个全局变量对象就是 window 。
- 将 this 指针指向这个全局对象。一个程序中只能存在一个全局执行上下文。
- 函数执行上下文(FEC) :每有函数被调用时,JavaScript 引擎都会为该函数创建一个新的函数执行上下文。它会按照执行上下文栈的规则依次调用。
- 由于函数可能多次调用,所以可能存在多个 FEC。
- Eval是特殊的函数上下文,由于
eval()
的安全问题,不推荐使用,故不作详细讨论。
a. ES5 组成结构
- 变量对象: 在函数执行前,函数内声明的所有变量(包括函数声明和 var 声明的变量)都会被添加到变量对象中,这个过程被称为变量提升 。
- 变量提升阶段,函数声明会被完全初始化,
var
声明的变量会被初始化为undefined
。
- 变量提升阶段,函数声明会被完全初始化,
- this 值: 指在 JavaScript 中函数执行时,this 关键字所引用的对象。这个对象是动态决定的,并且可以通过不同的调用方式(如直接调用、方法调用、构造函数调用、apply/call 调用)改变。
b. ES6+ 组成结构
- 变量环境: 在 ES6 中,变量环境主要用于处理 var 声明的变量。它的行为和变量对象类似, 但其中函数的定义改为非块级作用域函数。
- 词法环境: 跟变量环境同级,是 ES6 新增用来存储当前环境中存在的所有局部变量、函数声明、函数参数的。这些变量和函数在它们的声明位置才会被添加到词法环境中(无变量提升)。还包含对外部环境的引用,它用于实现作用域链,即如果在当前词法环境中找不到某个变量,就会沿着这个引用去外部环境中查找。
- this 值:this 的行为在大多数情况下与 ES5 是相同的,但 ES6+ 中新增箭头函数,所以中有所不同。
JS
// 在 ES5 中,每个新函数都定义了它自己的 this 值(一个新的函数执行上下文)。
function PersonES5() {
this.age = 0;
setInterval(function growUp() {
// 在非严格模式的函数中,this 引用的是全局对象,不是 PersonES5 对象。
// this.age++ 会在全局对象上创建一个 age 属性,并将其值设为 undefined。
this.age++; // undefined + 1 的结果是 NaN, NaN 值被赋给全局的 age 属性。
}, 1000);
}
// 在 ES6+ 中,箭头函数不会创建自己的 this 上下文,所以 this 从它的父执行上下文中继承。
function PersonES6() {
this.age = 0;
setInterval(() => {
// 这个箭头函数不创建自己的 `this` 上下文,所以 `this` 是从父执行上下文中继承的,即 PersonES6 对象。
this.age++;
}, 1000);
}
⚠️ 注意
-
虽然从连线看,「变量对象」 or 「词法环境 + 变量环境」是组成作用域链的一部分,但这不意味它们完全等同于作用域:
- 作用域是一个概念性的东西,不是实体。 用于描述变量和函数的可访问性。
- 「变量对象」 or 「词法环境 + 变量环境」是存储变量和函数的具体结构,它在 JavaScript 引擎内部用于跟踪标识符(变量和函数的名字)。
-
市面上很多人的文章里都提到说执行上下文包含三部分:变量对象、this指针、作用域链,这其实是不准确的。
- 作用域链也是一个概念性的东西,不是实体。 所以说是组成部分不合理
- 作用域链描述的是在 JavaScript 中查找变量与函数的规则。是通过词法环境(或 ES5 的变量对象)中的
[[Environment]]
属性实现链条式访问的,它本身没有一个具体数据结构存了这种访问路径,而是一级一级的[[Environment]]
属性组合才实现了链的形式。
c. 生命周期
执行上下文的生命周期关注的是单个函数调用或全局环境的生命周期,包括三个阶段:
- 创建:当函数被调用时,首先会创建一个新的执行上下文。在这个阶段,会确定 this 的值,创建变量对象,收集函数内声明的变量和函数,并且建立作用域链。
- 执行:在代码执行阶段,变量赋值发生,函数表达式会被解析并准备好被调用,上下文中的代码被逐行执行。
- 回收:当函数执行完毕并返回结果后,执行上下文进入回收阶段。在这个阶段,如果没有其他引用指向这个执行上下文(例如闭包),它将成为垃圾回收的候选对象,并最终被销毁。
2. 执行上下文栈(Execution Context Stack)
也称为调用栈,后进先出,用于存储在代码执行期间创建的所有执行上下文。
它是 JS 引擎追踪函数调用和执行进程的机制,记录了程序中函数的调用顺序。栈的生命周期,与单个上下文的生命周期密切相关,并涉及到多个上下文。
a. 生命周期
执行上下文栈的生命周期,关注的是整个程序中所有执行上下文的集合。
它描述了执行上下文如何随着函数调用和返回而入栈和出栈的全过程。基于下面这段 JS 代码解释:
JS
var name = "Victor";
function first() {
var a = "Hi!";
second();
console.log(`${a} ${name}`);
}
function second() {
var b = "Hey!";
third();
console.log(`${b} ${name}`);
}
function third() {
var c = "Hello!";
console.log(`${c} ${name}`);
}
first();
ⅰ. 初始化
当 JavaScript 程序开始执行时,全局执行上下文被创建并推入一个初始为空的栈中,这标志着执行上下文栈的开始:
- JS 引擎加载上述代码,创建 GEC 放到调用栈顶端,并声明
name
变量。 - JS 引擎执行 first 函数,创建 first FEC,继续放在调用栈顶端。谁在上面执行谁,所以 GEC 暂停,开始执行
first()
内部逻辑,a
变量位于 first 函数执行上下文中。 first()
内部,遇到second()
调用,创建 second FEC,继续放在调用栈顶端。谁在上面执行谁,所以 first 函数暂停,执行 second 函数内部逻辑,b
变量在 second 函数执行上下文中。- 同理,创建 third FEC,c 变量在
third()
执行上下文中。
此时我们绘制调用栈如图:
ⅱ. 过程阶段
(函数调用、代码执行、函数返回)从栈顶依次执行代码,随着函数的调用和返回,新的执行上下文从栈顶移除并销毁。
ⅲ. 清理阶段
当程序执行完成,全局执行上下文也会从栈中弹出,此时栈被清空,整个程序的执行结束。
二、作用域相关
1. 作用域(Scope)
一句话概述:作用域定义了变量和函数的可访问范围,是一个概念而非实体。
a. 分类
- 全局作用域:访问无局限性,代码任何地方都可以访问到全局作用域中声明的变量。
- 局部作用域:访问有局限性。
- 函数作用域:在函数内部声明的变量,只能在该函数内部访问。
- 块级作用域(ES6+):通过 let 和 const 声明,在 { } 内部访问。
b. 主要作用
- 避免命名冲突:通过作用域,我们可以在不同的区域定义同名的变量,而不会产生冲突。
- 控制变量的生命周期:变量在其作用域内是活动的,一旦离开这个作用域,就无法访问这个变量,这有助于内存管理。
2. 作用域链(Scope Chain)
一句话概述:作用域链是 JS 查找变量和函数的机制,是一个概念而非实体。
a. 运行机制
当引用一个变量时,JavaScript 首先在当前作用域查找
- 如果当前作用域未找到该变量,它会沿着内部属性
[[Environment]]
向上查找,它指向了当前词法环境的父级词法环境(ES5中是变量对象)。多个[[Environment]]
属性就形成了一个链式结构,我们称之为作用域链。 - 如果在全局环境中仍未找到,将报错,提示变量未定义。
⚠️ 注意:很多资料中会出现
[[Scope]]
和[[Environment]]
二者的混淆。
我的理解 [[Environment]]
是 ES 规范中定义的属性,而 [[Scopes]]
是某些 JavaScript 引擎(比如Chrome)该属性的具体实现。二者表达的本质是一回事,可能有细微差别,但可忽略不计。
stackoverflow 也有人说[[Scope]]
是[[Environment]]
的旧称,大家见仁见智理解。
三、变量提升
一句话概述:是指变量、函数声明在代码执行之前,被移动到它们所在作用域的顶部,使得无论声明它们的位置在哪,它们都可以在声明之前被访问。
⚠️ 注意:两个点
- 被提升到所在的作用域顶部,而不是作用域链的顶部:
- 如果你在全局作用域中使用 var 声明一个变量,那么这个变量会被提升到全局作用域的顶部。
- 如果你在一个函数内部使用 var 声明一个变量,那么这个变量会被提升到这个函数作用域的顶部。
- 虽然变量名会被提升,但是变量的初始化值(即赋值操作)不会被提升。所以在变量声明之前,访问这个变量会得到
undefined
。
四、闭包(Closures)
一句话概述: 闭包是一种特性,能够使函数访问其自身定义域之外的变量。
常见表现形式是,一个函数(我们称之为外部函数)返回另一个函数(内部函数)。这个内部函数可以访问外部函数的变量,即使外部函数已经执行完毕。
1. 运行机制
在 JavaScript 中,每次函数调用都会创建新的执行上下文,此上下文包含函数参数、局部变量、this 绑定和对外部环境的引用:
- 对外部环境的引用:即
[[Environment]]
属性,它保存了函数创建时的词法环境,包括所有局部变量、参数和块级作用域内的函数声明,是闭包的关键。
当一个函数在另一个函数内部定义时,内部函数会保存对外部函数执行上下文的引用,形成闭包。
函数完成执行后,它的执行上下文会被从执行上下文栈中弹出。然而,如果存在一个闭包,那么这个词法环境会被保留,不会被垃圾回收,因为内部函数仍持有对它的引用。这就是内部函数能访问外部函数变量的原因,即使外部函数已执行完毕。
然后,当这个内部函数在后续被调用时,无论它在哪里被调用,它都可以通过这个[[Environment]]
属性访问到原始的词法环境。例如以下代码:
JS
function outerFun(outerVar) {
return function innerFun(innerVar) {
console.log('outerVar:', outerVar);
console.log('innerVar:', innerVar);
}
}
const newFunction = outerFun('outside');
newFunction('inside'); // logs: outerVar: outside, innerVar: inside
在这个例子中,outerFun 创建并返回了 innerFun。
- innerFun 是一个闭包,因为它可以访问到 outerFun 的作用域,所以它可以访问 outerVar
- 调用
outerFun('outside')
得到newFunction()
。这个新的函数其实就是innerFun(innerVar){ ... }
- 然后调用
newFunction('inside')
, innerFun 通过[[Environment]]
访问外部函数的词法环境,即使 outerFun 已经执行完毕,但 newFunction 仍然可以访问到 outerVar
这就是闭包的关键特性:一个函数可以记住并访问它自己被定义时的环境,即使它在这个环境之外被调用。
⚠️ 注意:闭包的本质是函数,这句话是不准确的。
当我们说 "某个函数是一个闭包" 时,本身想表达的意思是这个函数是在另一个函数的作用域中定义的,并且它可以访问并记住这个外部作用域。
- 这种表述本身,其实是一种不准确的习惯性说法。
- 闭包本质是一种特性,并非为函数。
- 但由于闭包常见表现形式是通过外部函数返回内部函数实现,才导致产生了"闭包是函数"这种抄近路说法。
五、原型(Prototype)相关
一句话概述原型: 原型是一个对象,它提供了一种机制,允许对象共享另一个对象(即其原型)的属性和方法,这样可以避免在每个对象中重复定义相同的属性和方法。
一句话概述原型链:本身就是一种用于查找的机制,当访问一个对象的属性或方法时,如果对象自身没有,JavaScript 会沿着这个对象的原型链向上查找,直到找到这个属性或方法或者达到原型链的末端(null)。
1. 属性与方法
JS
let person = {
name: '张三',
age: 35,
steps: 0,
talk: function() {
console.log('Hello, my name is ' + this.name);
},
run: function() {
this.steps++;
}
};
- 属性:对象的状态,如 "姓名"、"年龄"、"步数"
- 方法:对象的行为,如 "说话"、"跑步"
2. 与继承的关系
原型提供了继承的基础,而原型链提供了实现继承的查找机制,它们共同实现了JavaScript的继承 + 属性/方法的查找。
3. 三个重要属性
想弄明白原型与链,首先要先搞清楚这三个属性:constructor、proto、prototype。
a. constructor
constructor 是对象的属性,它存在于每个被创建的对象中,并指向创建该对象的构造函数。
在下述的例子中
- arr 是被创建的新数组,那么 arr 的 constructor 属性就会指向 Array 函数。
- person 是 Person 类型的一个实例,所以它的 constructor 属性指向 Person 函数。
JS
let arr = new Array();
console.log(arr.constructor === Array); // 输出:true
function Person(name) {
this.name = name;
}
let person = new Person('张三');
console.log(person.constructor === Person); // 输出:true
b. prototype
prototype 是函数独有的属性,其本身是一个对象。包含了通过当前函数创建的所有对象实例可以共享的属性和方法。因为 prototype 自己是对象,所以它有属性 constructor,指向函数本身。
JS
function Person(name) {
this.name = name;
}
console.log(Person.prototype.constructor === Person); // 输出 true
当你创建一个新实例对象时,这个实例会有一个内部链接,指向它的构造函数的 prototype 。当你试图访问对象实例的某个属性或方法时,如果对象实例本身没有这个属性或方法,JavaScript 就会在 prototype 对象上查找。
JS
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log('Hello, my name is ' + this.name);
};
let person = new Person('张三');
person.sayHello(); // 输出:"Hello, my name is 张三"
⚠️ 注意 :
prototype
属性和原型(Prototype)虽然有关联,但它们指的并不是同一个东西。
prototype
:是函数独有的一个属性,本身是个对象包含一些属性和方法。- Prototype 想表达的是:我的原型是谁 ,所以应该对应
[[Prototype]]
属性,指向创建这个对象的构造函数的 prototype 对象。是对象都有的一个属性(也称为隐式原型),。
对于对象来说,由于没有prototype
,所以二者不一致很好理解。
但是对于函数,由于函数本身是特殊的对象,所以它既有prototype
属性又有[[Prototype]]
属性。如何区分二者呢?也很好理解,因为所有的函数在 JavaScript 中都是 Function 的实例,所以如下图:
函数的prototype
跟自己有关,函数的[[Prototype]]
跟 Function 有关
c. proto
__proto__
是对象的一个属性,指向创建该对象的构造函数的 prototype 属性,即该对象的原型。
下图跟上面的图很像对吧,因为 __proto__
和[[Prototype]]
在 JavaScript 中基本上是一回事。
当你试图访问一个对象的某个属性/方法时,如果该对象本身没有这个属性/方法,那么 JavaScript 会沿着 __proto__
链(也就是原型链)向上查找,直到找到这个属性/方法或者查找到 null。这个属性实现了原型链的查找机制。
JS
let arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype); // 输出:true
特例: Function 是一个特殊的构造函数,它既是一个函数,也是自己的实例。
因此,Function 的 __proto__
(或者说 [[Prototype]] )指向的是 Function.prototype ,这是因为在JavaScript中,所有的函数都是 Function 的实例,包括 Function 自身。
其他的构造函数(如Object、Array等)的 __proto__
属性并不等于它们自身的 prototype 属性
4. 一图理清原型链
结合上面一步步讲解,我们现在讲这三个关键属性合并填在图上,并新增 Object 部分内容。
"一切皆为对象" ,在 JavaScript 中 Object 是所有对象的最终原型。
当我们创建一个新的对象,无论是字面量创建,还是通过构造函数创建,这个新对象的原型([[Prototype]]
或 __proto__
)都会被设置为构造函数的 prototype 属性。如果我们沿着 __proto__
(原型链)向上查找,最终都会找到 Object.prototype。
Object.prototype 的原型是 null ,表示原型链的结束。
JS
function Person(name) {
this.name = name;
}
var person = new Person('张三');
// person.__proto__ === Person.prototype
// Person.prototype.__proto__ === Object.prototype
// Object.prototype.__proto__ === null
⚠️ 注意:为什么 Object.constructor 和 proto 会指向 Function 的方向?
- 因为Object 是一个构造函数,用于创建新的对象。因为所有的构造函数都是函数,所以 Object.constructor === Function。
- 因为
__proto__
指向构造函数的prototype。对于Object,它的构造函数是 Function,所以Object.__proto__
=== Function.prototype。
这种情况不仅适用于Object,对于所有的内置构造函数(如Array、Date等)以及用户自定义的构造函数,它们的constructor 属性都指向 Function 。
5. 原型链与作用域链的异同点
原型链和作用域链都是 JavaScript 中用于查找变量和函数的机制,但它们的用途和工作方式有所不同。
- 原型链用于属性/方法查找 + 实现继承。当你试图访问一个对象的属性时,如果该对象本身没有这个属性,那么 JavaScript 会在该对象的原型(即它的父对象)上查找该属性,如果还没有,就会继续在原型的原型上查找,以此类推,直到找到属性或者到达原型链的末端(null)
- 作用域链单纯用于查找变量和函数。当你在一个函数内部试图访问一个变量时,JavaScript 首先会在当前函数的作用域中查找该变量,如果没有找到,就会去父作用域查找,然后是父作用域的父作用域,以此类推,直到找到变量或者到达全局作用域。
相同点
- 它们都是链式结构,都可用于查找属性、变量或函数。
- 它们都从子到父进行查找。
不同点
- 原型链除了查找外,比作用域链多一个实现继承的能力。
- 原型链是在对象上进行查找,作用域链是在执行上下文的作用域中进行查找。
- 原型链的末端是
null
,作用域链的末端是全局作用域。
六、继承(Inheritance)
JavaScript 继承是一种基于原型链的机制,使得对象可以访问和使用其原型对象的属性和方法。
截止 2024.04
JavaScript 的所有继承方式如下:
1. 原型链继承(Prototype Chain)
当涉及原型链继承时,理解构造函数、原型和实例之间的关系是至关重要的。先去看原型链
最基本的继承方式,通过将子类的原型设置为父类的实例来实现继承:
JS
function Parent() {
this.property = true;
}
Parent.prototype.getMethod = function() {
return this.property;
};
function Child() {
this.childProperty = false;
}
// 继承Parent
Child.prototype = new Parent();
优点:
- 简单易懂,易于实现。
- 方法自然被所有实例共享。
缺点:
- 共享属性问题: 原型链继承最大的问题是所有子对象共享同一个原型对象,因此,如果一个子对象修改了继承的引用类型属性(如数组或对象),则会影响到其他子对象,这可能会导致意外的行为。
- 不能传递参数: 在使用原型链继承时,不能向父类构造函数传递参数,因为实例化子类时实际上是调用了父类构造函数,而不是传递参数的方式。
- 无法实现多继承: JavaScript 的原型链继承只支持单继承,一个子类只能继承一个父类的属性和方法。
2. 构造函数继承(Constructor Stealing)
通过在子类的构造函数中调用父类构造函数,可以继承父类的属性,但不会继承父类原型上的属性和方法:
JS
function Parent(name) {
this.name = name;
}
function Child(name) {
Parent.call(this, name);
}
优点:
- 可以在子类构造函数中向父类传递参数。
- 父类方法可以在子类构造函数中重新定义,实现方法的重写。
缺点:
- 方法都在构造函数中定义,每次创建实例时都会创建一遍方法,无法实现函数复用,这会导致性能下降。
- 只能继承父类的实例属性和方法,不能继承原型属性/方法。
3. 原型式继承(Prototypal Inheritance)
通过创建一个临时的构造函数来继承,借助 Object.create()
方法创建一个新对象,这个新对象的原型(prototype)是另一个已存在的对象,从而实现继承。
JS
let parent = {
name: "parent",
friends: ["p1", "p2", "p3"],
getName: function() {
return this.name;
}
};
let person1 = Object.create(parent);
person1.name = "tom";
person1.friends.push("jerry");
let person2 = Object.create(parent);
person2.friends.push("lucy");
console.log(person1.name); // tom
console.log(person1.name === person1.getName()); // true
console.log(person2.name); // parent1
console.log(person1.friends); // ["p1", "p2", "p3","jerry","lucy"]
console.log(person2.friends); // ["p1", "p2", "p3","jerry","lucy"]
上述例子,person1 和 person2 都通过 Object.create(parent) 继承了 parent 对象。
优点:
- 简单易用: 原型式继承是一种简单的方式来创建对象之间的继承关系,无需定义构造函数或类。
- 共享属性和方法: 由于多个实例共享原型对象上的属性和方法,这可以节省内存空间。
缺点:
- 共享引用类型属性: 原型式继承会导致多个实例共享引用类型属性,如果一个实例修改了这个属性,其他实例也会受到影响,可能引发意外的副作用。
4. 组合继承(Combination Inheritance)
结合原型链继承和构造函数继承,将两者的优点结合在一起,组合继承解决了原型链和构造函数继承的问题,是JavaScript中最常用的继承模式。
JS
function Parent(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.sayName = function() {
console.log(this.name);
};
function Child(name, age) {
Parent.call(this, name); // 继承属性
this.age = age;
}
Child.prototype = new Parent(); // 继承方法
Child.prototype.constructor = Child; // 修正构造函数指向
优点:
- 融合原型链继承和构造函数继承的优点。
- 可以继承父类原型上的属性和方法,也可以传递参数,可复用。
- 可以实现方法的重写。
- 每个新实例引用的属性都是私有的。
缺点:
- 调用了两次父类构造函数,生成了两份实例(子类实例将父类实例属性又复制了一份)。
- 子类的原型上会多出不必要的、多余的父类实例属性。
5. 寄生式继承(Parasitic Inheritance)
创建一个仅用于封装继承过程的函数:
JS
function createAnother(original) {
var clone = Object.create(original); // 通过调用函数创建一个新对象
clone.sayHi = function() { // 以某种方式来增强这个对象
console.log('hi');
};
return clone; // 返回这个对象
}
6. 寄生组合式继承(Parasitic Combination Inheritance)
通过借用构造函数来继承属性,而通过原型链的混成形式来继承方法。是对组合继承的优化。它解决了组合继承两次调用父类构造函数的问题。,这种方式是目前在不支持ES6的环境下最为理想的继承方式。
JS
function inheritPrototype(childObject, parentObject) {
var prototype = Object.create(parentObject.prototype); // 创建对象
prototype.constructor = childObject; // 增强对象
childObject.prototype = prototype; // 指定对象
}
优点:
- 解决了组合继承的效率问题,只调用一次父类构造函数,并且因此避免在Child.prototype上创建不必要的、多余的属性。
- 与组合继承相比,效率更高,内存占用更少。
缺点:
- 实现较为复杂。
- 需要额外的封装函数。
7. ES6 类继承(Class Inheritance)
ES6 引入了class关键字,提供了更接近传统面向对象语言的写法,使得继承在语法上更加直观和易于理解:
JS
class Parent {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
优点:
- 语法更清晰,更接近传统的面向对象编程语法,易于理解和使用。
- 提供了基于类的面向对象编程方式。
- 解决了原先原型和构造函数继承的复杂性。
- class语法内部实现了寄生组合式继承,因此性能良好。
缺点:
- ES6 语法并不是所有环境都支持,尽管可以通过转译器(如Babel)转换为ES5代码,但这可能影响调试和性能。
总结而言,每种继承方式都有其适用场景,寄生组合式继承通常被认为是最理想的继承方式,因为它几乎涵盖了所有优点,同时避免了组合继承的缺点。而随着ES6的普及,类继承已经成为了开发中的首选方式,因为它提供了一种更加直观、易于理解和实现的继承语法。
七、参考文档
- 《JavaScript深入之执行上下文栈》
- 《JavaScript Execution Context》-- How JS Works Behind The Scenes
- 《你真的了解执行上下文吗?》
- 《一张图搞定JS原型&原型链》