原型、原型链
在 JavaScript 中,每一个对象都有一个特殊的属性 __proto__,这个属性指向该对象的原型。原型本身也是一个对象,它可能也有自己的原型,这样就形成了一条链。
当访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript 会沿着原型链查找,直到找到该属性为止。如果整个原型链中都没有找到该属性,则返回 undefined。
闭包
闭包的基本定义
在 JavaScript 中,闭包是指一个函数可以访问并操作它创建时所在作用域内的变量,即使该函数在外部作用域中被调用。换句话说,闭包是由函数及其相关的引用环境组合而成的一个实体。
闭包的工作原理
当一个函数在一个作用域中定义时,它会记住创建时所处的作用域中的所有变量。即使该函数在另一个作用域中被调用,它仍然可以访问那些变量。这是因为 JavaScript 的执行环境会保留对这些变量的引用,直到没有引用指向它们为止。
闭包的优点
- 封装性:闭包可以用来封装变量,使其不被外部作用域访问,从而保护数据不被外部修改。
- 持久性:闭包可以让变量保持在内存中,直到没有函数引用它们为止,这样可以在多次调用之间保持状态。
- 模块化:通过闭包可以实现模块化的设计模式,将一组相关的函数和数据封装在一起。
闭包的注意事项
- 内存泄漏:如果闭包中引用了大量数据,且长时间未被垃圾回收器清理,可能会导致内存占用过高。因此,在使用闭包时需要注意释放不再使用的变量。
- 性能影响:由于闭包使得函数可以访问外部作用域中的变量,每次调用闭包函数时都需要查找作用域链,这可能会影响性能。
实际应用
闭包在实际开发中有广泛的应用,例如:
- 模块模式:使用闭包来创建私有的变量和函数,只暴露必要的接口。
- 事件处理程序:在事件处理中,闭包可以记住事件发生时的状态。
- 迭代器和生成器:使用闭包来创建迭代器和生成器,保持内部状态。
作用域链
作用域链(Scope Chain)是 JavaScript 中一个重要的概念,它决定了变量的可访问性和查找机制。理解作用域链对于正确地管理变量的作用域至关重要。
作用域链的基本概念
作用域链是一个指针链表,它连接了当前执行上下文(Execution Context)的变量环境(Variable Environment)与其所有父执行上下文的变量环境。当一个函数执行时,JavaScript 引擎会创建一个执行上下文,并将其加入到当前的作用域链中。
作用域链的组成
作用域链通常由以下部分组成:
- 当前执行上下文的变量环境:包含当前执行上下文中的局部变量和参数。
- 父执行上下文的变量环境:如果是函数调用,则包含上一层函数的变量环境。
- 全局执行上下文的变量环境:始终位于作用域链的末尾,包含全局变量。
查找过程
当尝试访问一个变量时,JavaScript 引擎会按照以下步骤进行查找:
- 首先在当前执行上下文的变量环境中查找。
- 如果没有找到,则沿着作用域链向上查找。
- 一直查找到全局执行上下文的变量环境。
- 如果在整个作用域链中都没有找到,则返回
undefined。
示例
让我们通过一个具体的例子来说明作用域链的工作原理:
function outer() {
var outerVar = "I'm in outer";
function inner() {
console.log(outerVar); // 访问外部作用域的变量
}
inner();
}
outer(); // 输出: I'm in outer
在这个例子中,当 outer 函数被调用时,会创建一个 outer 函数的执行上下文,并将其加入到作用域链中。当 inner 函数被调用时,也会创建一个 inner 函数的执行上下文,并将其加入到作用域链中。此时的作用域链如下所示:
current (inner) -> parent (outer) -> global
当 inner 函数尝试访问 outerVar 时,它首先在自己的变量环境中查找,没有找到后,继续沿着作用域链向上查找。最终在 outer 函数的变量环境中找到了 outerVar,并输出了正确的结果。
更复杂的例子
让我们再看一个更复杂的例子,展示多层嵌套的作用域:
function outermost() {
var outermostVar = "I'm in outermost";
function outer() {
var outerVar = "I'm in outer";
function inner() {
console.log(outermostVar); // 访问最外层作用域的变量
console.log(outerVar); // 访问中间层作用域的变量
}
inner();
}
outer(); // 输出: I'm in outermost 和 I'm in outer
}
outermost();
在这个例子中,当 outermost 函数被调用时,创建了 outermost 的执行上下文。当 outer 函数被调用时,创建了 outer 的执行上下文。最后,当 inner 函数被调用时,创建了 inner 的执行上下文。此时的作用域链如下所示:
current (inner) -> parent (outer) -> grandparent (outermost) -> global
当 inner 函数尝试访问 outermostVar 时,它沿着作用域链查找,最终在 outermost 函数的变量环境中找到了 outermostVar。同样,当 inner 函数访问 outerVar 时,它在 outer 函数的变量环境中找到了 outerVar。
词法作用域、动态作用域
词法作用域(Lexical Scope)是 JavaScript 中一个基本的概念,它决定了变量在何处可以被访问。词法作用域是一种静态作用域,意味着变量的作用域是在编写代码时就已经确定的,而不是在运行时动态决定的。
词法作用域的基本概念
词法作用域指的是变量的可访问范围(即作用域)是由变量声明的位置决定的。具体来说,函数内部可以访问其外部函数声明的变量,但外部函数不能访问内部函数声明的变量。这是因为函数在定义时就已经确定了它可以访问哪些变量。
词法作用域 vs 动态作用域
与词法作用域相对的是动态作用域(Dynamic Scope),在动态作用域中,变量的作用域是在运行时根据调用栈来决定的。而在 JavaScript 中,使用的是词法作用域。
示例
下面通过几个例子来说明词法作用域的工作原理:
示例 1:基本的词法作用域
function outer() {
var outerVar = "I'm in outer";
function inner() {
console.log(outerVar); // 输出: I'm in outer
}
inner();
}
outer();
在这个例子中,inner 函数可以访问 outer 函数作用域中的变量 outerVar,因为 inner 函数是在 outer 函数内部定义的。
示例 2:嵌套函数
function outermost() {
var outermostVar = "I'm in outermost";
function outer() {
var outerVar = "I'm in outer";
function inner() {
console.log(outermostVar); // 输出: I'm in outermost
console.log(outerVar); // 输出: I'm in outer
}
inner();
}
outer();
}
outermost();
在这个例子中,inner 函数可以访问 outer 函数作用域中的变量 outerVar,同时也可以访问 outermost 函数作用域中的变量 outermostVar。这是因为 inner 函数的作用域链包括了 outer 函数的作用域和 outermost 函数的作用域。
示例 3:函数声明与调用分离
function outer() {
var outerVar = "I'm in outer";
var inner = function() {
console.log(outerVar); // 输出: I'm in outer
};
inner();
}
outer();
在这个例子中,尽管 inner 函数是在 outer 函数内部定义的,但它仍然可以访问 outer 函数作用域中的变量 outerVar,因为它的作用域是在定义时确定的。
闭包与词法作用域
闭包(Closure)是词法作用域的一种常见应用。闭包是指一个函数可以访问其创建时所在的词法作用域中的变量。即使这个函数在其他地方被调用,它仍然可以访问原始作用域中的变量。
示例 4:闭包
function outer() {
var counter = 0;
function increment() {
counter++;
console.log(counter); // 输出: 1, 2, 3...
}
return increment;
}
var closureExample = outer();
closureExample(); // 输出: 1
closureExample(); // 输出: 2
closureExample(); // 输出: 3
在这个例子中,increment 函数是一个闭包,它可以访问并修改 outer 函数作用域中的变量 counter。即使 increment 函数在 outer 函数外部被调用,它仍然可以访问 outer 函数作用域中的变量。
执行上下文
执行上下文(Execution Context)是 JavaScript 中一个非常重要的概念,它描述了在执行代码时 JavaScript 引擎的工作环境。执行上下文决定了变量和函数的可访问性,以及当前执行的代码段。理解执行上下文对于深入理解 JavaScript 的运行机制至关重要。
执行上下文的类型
执行上下文主要有三种类型:
- 全局执行上下文:当 JavaScript 代码开始执行时,默认进入的第一个执行上下文。
- 函数执行上下文:每当一个函数被调用时,都会创建一个新的函数执行上下文。
- Eval执行上下文:当使用
eval()函数执行字符串形式的代码时,会创建一个新的 Eval 执行上下文。
执行上下文的生命周期
执行上下文的生命周期可以分为三个阶段:
- 创建阶段:
-
- 变量环境(Variable Environment) :创建阶段会初始化变量环境,存储当前执行上下文中的变量和函数声明。
- 作用域链(Scope Chain) :创建阶段还会初始化作用域链,用于确定变量的可访问性。
- this绑定:在创建阶段还会确定
this关键字的绑定。全局执行上下文中的this通常指向全局对象(通常是window),而函数执行上下文中的this则取决于函数的调用方式。
- 执行阶段:
-
- 在执行阶段,代码被执行,变量被赋值,函数被调用。执行上下文中的变量和函数在此阶段发挥作用。
- 销毁阶段:
-
- 当执行上下文完成其任务后,会被销毁。对于全局执行上下文,它通常不会被销毁;而对于函数执行上下文,一旦函数执行完毕,就会被销毁。
执行上下文栈
JavaScript 引擎使用一个执行上下文栈来管理执行上下文。每当进入一个新的执行上下文时,都会将该上下文压入栈顶;当退出一个执行上下文时,会将该上下文从栈顶弹出。执行上下文栈确保了执行上下文的正确管理和清理。
示例
下面通过一个具体的例子来说明执行上下文的工作原理:
function outer() {
var outerVar = "I'm in outer";
function inner() {
console.log(outerVar); // 输出: I'm in outer
}
inner();
}
outer();
在这个例子中,执行上下文的创建和销毁过程如下:
- 全局执行上下文:
-
- 创建全局执行上下文。
- 初始化全局作用域链(仅包含全局变量环境)。
- 执行全局代码,定义
outer函数。
- 函数执行上下文:
-
- 当
outer函数被调用时,创建outer函数执行上下文。 - 初始化
outer函数的作用域链(包含全局变量环境和outer函数的变量环境)。 - 执行
outer函数的代码,定义inner函数,并调用inner函数。
- 当
- 内层函数执行上下文:
-
- 当
inner函数被调用时,创建inner函数执行上下文。 - 初始化
inner函数的作用域链(包含全局变量环境、outer函数的变量环境和inner函数的变量环境)。 - 执行
inner函数的代码,输出outerVar的值。
- 当
- 销毁执行上下文:
-
inner函数执行完毕后,销毁inner函数执行上下文。outer函数执行完毕后,销毁outer函数执行上下文。
执行上下文栈
执行上下文栈(Execution Context Stack)是 JavaScript 引擎用来管理执行上下文的一个关键数据结构。理解执行上下文栈有助于更好地理解 JavaScript 的执行机制,特别是函数调用和作用域的相关概念。
执行上下文栈的作用
执行上下文栈用于跟踪当前正在执行的函数调用。每当一个函数被调用时,一个新的执行上下文(Execution Context)就会被创建并压入栈顶;当该函数执行完毕后,执行上下文就会从栈中弹出。执行上下文栈确保了函数调用的正确顺序,并且管理着当前执行上下文的生命周期。
执行上下文栈的组成
执行上下文栈中的每个执行上下文包含以下信息:
- 变量环境(Variable Environment) :存储当前上下文中的变量和函数声明。
- 作用域链(Scope Chain) :用于确定变量的可访问性。
- this绑定:确定当前上下文中
this关键字的值。 - 代码执行上下文:当前执行的代码段。
执行上下文栈的生命周期
执行上下文栈的生命周期可以分为以下几个阶段:
- 全局执行上下文:
-
- 当 JavaScript 代码开始执行时,首先创建全局执行上下文。
- 初始化全局作用域链(仅包含全局变量环境)。
- 执行全局代码。
- 函数执行上下文:
-
- 每次函数被调用时,都会创建一个新的函数执行上下文并压入执行上下文栈。
- 初始化函数的作用域链(包含全局变量环境和当前函数的变量环境)。
- 执行函数代码。
- 销毁执行上下文:
-
- 当函数执行完毕后,函数执行上下文从栈中弹出。
- 最终,当所有函数调用都完成后,全局执行上下文成为唯一的执行上下文。
示例
下面通过一个具体的例子来说明执行上下文栈的工作原理:
function outer() {
var outerVar = "I'm in outer";
function inner() {
console.log(outerVar); // 输出: I'm in outer
}
inner();
}
outer();
在这个例子中,执行上下文栈的操作如下:
- 创建全局执行上下文:
-
- 创建全局执行上下文。
- 初始化全局作用域链。
- 定义
outer函数。
- 创建
outer函数执行上下文:
-
- 当
outer函数被调用时,创建outer函数执行上下文并压入栈顶。 - 初始化
outer函数的作用域链。 - 定义
inner函数并调用inner函数。
- 当
- 创建
inner函数执行上下文:
-
- 当
inner函数被调用时,创建inner函数执行上下文并压入栈顶。 - 初始化
inner函数的作用域链。 - 输出
outerVar的值。
- 当
- 销毁执行上下文:
-
inner函数执行完毕后,销毁inner函数执行上下文,并从栈中弹出。outer函数执行完毕后,销毁outer函数执行上下文,并从栈中弹出。
执行上下文栈的可视化
我们可以将执行上下文栈的可视化表示如下:
全局执行上下文
|
+-- outer 函数执行上下文
| |
| +-- inner 函数执行上下文
|
在这个例子中,全局执行上下文始终存在,当 outer 函数被调用时,创建 outer 函数执行上下文并压入栈顶;当 inner 函数被调用时,创建 inner 函数执行上下文并再次压入栈顶。当函数执行完毕后,执行上下文依次从栈中弹出。
ES6
let和const
let 用于声明变量,这些变量具有块级作用域。这意味着它们只能在声明它们的代码块内访问。块级作用域通常是指 {} 内的代码块,如 if 语句或循环体。
let 的特点:
- 块级作用域:使用
let声明的变量只能在声明它们的代码块内访问。 - 暂时性死区(Temporal Dead Zone, TDZ) :在
let声明的变量之前访问该变量会导致 ReferenceError。这是因为在变量声明之前,该变量处于暂时性死区。 - 不允许重复声明:在同一作用域内,使用
let无法重复声明同一个变量。 - 提升但不可初始化:与
var不同,let声明的变量不会被提升并初始化为undefined。相反,它会处于暂时性死区。
const 的特点:
- 块级作用域:与
let类似,const声明的变量也是块级作用域。 - 暂时性死区(TDZ) :与
let相同,const声明的变量在声明之前访问也会导致 ReferenceError。 - 不允许重新赋值:一旦使用
const声明一个变量并赋值,就不能再次给它赋值。 - 不允许重复声明:在同一作用域内,使用
const无法重复声明同一个变量。
对比 var
与 var 相比,let 和 const 有几个显著的优势:
- 块级作用域:
var声明的变量具有函数级作用域,即在声明它们的函数体内任何位置都可以访问。而let和const声明的变量具有块级作用域,只能在声明它们的代码块内访问。 - 提升(Hoisting) :
var声明的变量会被提升至作用域顶部,并初始化为undefined。而let和const声明的变量不会被提升,并且在声明之前访问会导致 ReferenceError。 - 重复声明:
var允许在同一作用域内重复声明同一个变量,而let和const不允许这样做。
实际应用
在实际开发中,建议尽可能使用 let 和 const,因为它们提供了更好的作用域控制和安全性。以下是一些建议:
- 使用
const:如果你确定一个变量不会被重新赋值,那么应该使用const声明,以避免意外的重新赋值。 - 使用
let:如果你需要在一个作用域内声明一个变量,并且可能需要重新赋值,那么应该使用let。
模版字符串
模板字符串是 ES6 中一个非常实用的特性,它使得字符串的构造更加直观和灵活。通过模板字符串,你可以轻松地嵌入表达式、处理多行文本,并且利用标签模板函数自定义字符串的处理逻辑。理解模板字符串的用法,可以帮助你编写更清晰、更易维护的代码。
箭头函数
箭头函数提供了一种更简洁的语法来定义函数,并且具有以下特点:
- 简洁的语法:可以更简洁地定义函数,尤其是当函数体只有一个表达式时。
this绑定:箭头函数内部的this值是定义时所在的上下文的this值,而不是调用时的上下文。arguments对象:在箭头函数中,arguments对象是未定义的,可以使用剩余参数(rest parameters)来代替。
迭代器 for of
迭代协议
迭代协议要求对象实现一个名为 [Symbol.iterator] 的方法,该方法返回一个迭代器对象(Iterator),该迭代器对象实现了 next 方法。next 方法每次调用都会返回一个包含两个属性的对象:value 和 done。value 表示当前迭代的值,而 done 是一个布尔值,表示是否已经完成了迭代。
for...of 循环的基本用法
for...of 循环的基本语法如下:
for (const value of iterable) {
// 处理 value
}
示例
示例 1:遍历数组
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
console.log(number);
}
// 输出: 1, 2, 3, 4, 5
示例 2:遍历字符串
const text = "hello world";
for (const char of text) {
console.log(char);
}
// 输出: h, e, l, l, o, , w, o, r, l, d
示例 3:遍历 Map
const map = new Map([
['key1', 'value1'],
['key2', 'value2']
]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// 输出: key1: value1, key2: value2
示例 4:遍历 Set
const set = new Set([1, 2, 3, 4, 5]);
for (const item of set) {
console.log(item);
}
// 输出: 1, 2, 3, 4, 5
特点和优势
- 简洁性:
for...of循环提供了一种简洁的方式来遍历可迭代对象。 - 自动迭代:
for...of循环内部自动调用迭代器的next方法,直到done为true。 - 直接获取值:每次迭代直接获取当前迭代的值,不需要像传统
for循环那样手动索引或使用Array.prototype.forEach方法。
与 for...in 的区别
for...in 循环主要用于枚举对象的可枚举属性(包括原型链上的属性),而 for...of 用于遍历可迭代对象。
示例 5:对比 for...in 和 for...of
const obj = { a: 1, b: 2, c: 3 };
// 使用 for...in
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
console.log(`${key}: ${obj[key]}`);
}
}
// 输出: a: 1, b: 2, c: 3
// 使用 for...of
// 注意:普通对象不是可迭代对象,因此不能直接使用 for...of
// 若要使用 for...of 遍历对象的键或值,可以先将其转换为可迭代对象
const keys = Object.keys(obj);
for (const key of keys) {
console.log(`${key}: ${obj[key]}`);
}
// 输出: a: 1, b: 2, c: 3
实现可迭代对象
如果你想让自定义对象支持 for...of 循环,需要在对象中实现 [Symbol.iterator] 方法。
示例 6:自定义可迭代对象
class MyIterable {
constructor(values) {
this.values = values;
}
*[Symbol.iterator]() {
for (const value of this.values) {
yield value;
}
}
}
const myIterable = new MyIterable([1, 2, 3, 4, 5]);
for (const value of myIterable) {
console.log(value);
}
// 输出: 1, 2, 3, 4, 5
在这个例子中,MyIterable 类实现了 [Symbol.iterator] 方法,该方法返回一个生成器,生成器使用 yield 关键字来产生值。这样,myIterable 就成为了可迭代对象,可以使用 for...of 循环来遍历。
Async
异步编程(Asynchronous Programming)是现代 JavaScript 中非常重要的一个概念,特别是在处理网络请求、文件操作以及其他耗时操作时。ES6(ECMAScript 2015)引入了 Promise 作为一种处理异步操作的方式,而 ES7(ECMAScript 2016)则引入了 async/await 语法,使得异步代码更加简洁和易于理解。
async 函数
async 关键字用于定义一个异步函数。异步函数总是返回一个 Promise 对象,即使没有显式返回任何内容。如果异步函数内部抛出了错误,返回的 Promise 会变为 rejected 状态。
语法
async function functionName() {
// 异步操作
}
示例
async function fetchUser() {
try {
const response = await fetch('https://api.example.com/user');
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching user:', error);
throw error;
}
}
fetchUser().then(user => {
console.log(user);
}).catch(error => {
console.error('Failed to fetch user:', error);
});
await 表达式
await 关键字用于等待一个 Promise 完成。await 只能在 async 函数内部使用。
示例
async function fetchData() {
const response = await fetch('https://api.example.com/data');
const data = await response.json();
return data;
}
fetchData().then(data => {
console.log(data);
}).catch(error => {
console.error('Failed to fetch data:', error);
});
async/await 的优点
- 更简洁的语法:使用
async/await可以让异步代码看起来更像是同步代码,更容易理解和维护。 - 错误处理:使用
try/catch语句可以更自然地处理错误,而不是层层嵌套的.catch方法。 - 更易读的代码:相较于传统的回调地狱(Callback Hell)或层层嵌套的
Promise链,使用async/await的代码结构更加清晰。
示例:组合多个异步操作
async function processUser(userId) {
try {
const userProfile = await fetchUserProfile(userId);
const userPosts = await fetchUserPosts(userId);
const userData = {
profile: userProfile,
posts: userPosts
};
return userData;
} catch (error) {
console.error('Error processing user:', error);
throw error;
}
}
async function fetchUserProfile(userId) {
const response = await fetch(`https://api.example.com/user/${userId}/profile`);
return await response.json();
}
async function fetchUserPosts(userId) {
const response = await fetch(`https://api.example.com/user/${userId}/posts`);
return await response.json();
}
processUser('1234').then(userData => {
console.log('User data:', userData);
}).catch(error => {
console.error('Failed to process user:', error);
});
注意事项
- 错误处理:虽然
async/await提供了更自然的错误处理方式,但仍需注意捕获和处理错误。 - 性能:虽然
async/await使代码更简洁,但如果异步操作非常耗时,仍需考虑性能优化。 - 非
async函数中的await:await只能在async函数内部使用,否则会抛出语法错误。