深入理解JavaScript核心概念:迭代器、闭包与this关键字
前言
在探索JavaScript的深度时,我们需要理解几个核心概念:迭代器、闭包和this关键字。这些概念构成了JavaScript编程的基础,理解它们对于编写高效、可维护的代码至关重要。
迭代器与可迭代对象
迭代器模式
迭代器模式是一种标准化处理数据的方式,它允许我们逐个"块"地消费数据源。这种模式比一次性处理整个数据集更为常见和实用。
想象一个代表数据库查询结果的数据结构,通常组织为行。如果查询只有少量行,可以一次性处理整个结果集。但对于成百上千行的结果,就需要迭代处理(通常是循环)。
ES6迭代协议
ES6标准化了迭代器协议,定义了一个next()
方法,返回一个包含value
和done
属性的迭代器结果对象。done
是一个布尔值,在遍历完数据源之前为false
。
消费迭代器
ES6提供了几种消费迭代器的标准化方式:
for..of
循环:
for (let val of it) {
console.log(`迭代器值: ${val}`);
}
- 展开运算符
...
:- 数组展开:
var vals = [...it];
- 函数调用展开:
doSomethingUseful(...it);
可迭代对象
可迭代对象是可以被迭代的值。协议会自动从可迭代对象创建迭代器实例,并消费该实例直到完成。这意味着一个可迭代对象可以被多次消费,每次都会创建新的迭代器实例。
JavaScript中的基本数据结构/集合类型都是可迭代的,包括字符串、数组、Map、Set等。
示例
// 数组是可迭代的
var arr = [10, 20, 30];
for (let val of arr) {
console.log(`数组值: ${val}`);
}
// 使用展开运算符浅拷贝数组
var arrCopy = [...arr];
// 迭代字符串字符
var greeting = "Hello world!";
var chars = [...greeting];
Map的迭代
Map数据结构使用对象作为键,将值(任何类型)与该对象关联。Map的默认迭代不是仅遍历值,而是遍历其条目(键值对的元组)。
var buttonNames = new Map();
buttonNames.set(btn1, "Button 1");
buttonNames.set(btn2, "Button 2");
for (let [btn, btnName] of buttonNames) {
btn.addEventListener("click", function onClick() {
console.log(`点击了 ${btnName}`);
});
}
特定迭代
大多数内置可迭代对象提供三种迭代形式:
keys()
:仅键values()
:仅值entries()
:条目(键值对)
闭包
闭包的定义
闭包是函数记住并继续访问其作用域外变量的能力,即使函数在不同的作用域中执行。
闭包有两个定义特征:
- 闭包是函数本质的一部分(对象没有闭包,函数有)
- 要观察闭包,必须在不同于函数定义的作用域中执行函数
闭包示例
function greeting(msg) {
return function who(name) {
console.log(`${msg}, ${name}!`);
};
}
var hello = greeting("Hello");
hello("Kyle"); // Hello, Kyle!
在这个例子中,内部函数who()
闭包了外部函数greeting()
的msg
参数。即使greeting()
执行完毕,msg
变量仍然被保留在内存中,因为内部函数实例仍然存活。
闭包不是快照
闭包不是变量值的快照,而是变量本身的直接链接和保留。这意味着闭包可以观察(或制造)这些变量随时间的更新。
function counter(step = 1) {
var count = 0;
return function increaseCount() {
count += step;
return count;
};
}
var incBy1 = counter(1);
incBy1(); // 1
incBy1(); // 2
异步代码中的闭包
闭包在异步代码(如回调)中最为常见:
function getSomeData(url) {
ajax(url, function onResponse(resp) {
console.log(`响应 (来自 ${url}): ${resp}`);
});
}
即使getSomeData()
立即完成,url
参数变量也会在闭包中保持活动状态,直到Ajax调用返回并执行onResponse()
。
循环中的闭包
for (let [idx, btn] of buttons.entries()) {
btn.addEventListener("click", function onClick() {
console.log(`点击了按钮 (${idx})!`);
});
}
使用let
声明,每次迭代都会获得新的块作用域idx
和btn
变量。内部onClick()
函数闭包了idx
,保留它直到点击处理程序设置在btn
上。
this关键字
this的误解
关于this
有两个常见误解:
- 认为函数的
this
指向函数本身 - 认为
this
指向方法所属的实例
这两种理解都是错误的。
执行上下文
函数除了通过闭包附加到其封闭作用域外,还有另一个影响其访问能力的特性:执行上下文,通过this
关键字暴露给函数。
作用域是静态的,包含定义函数时可用的固定变量集。而函数的执行上下文是动态的,完全取决于如何调用它(无论在哪里定义或调用)。
this示例
function classroom(teacher) {
return function study() {
console.log(`${teacher} says to study ${this.topic}`);
};
}
var assignment = classroom("Kyle");
assignment(); // Kyle says to study undefined
当assignment()
作为普通函数调用时,this
默认为全局对象(非严格模式下),没有topic
属性,所以this.topic
解析为undefined
。
不同调用方式
- 作为方法调用:
var homework = {
topic: "JS",
assignment: assignment
};
homework.assignment(); // Kyle says to study JS
- 使用
call()
指定上下文:
var otherHomework = { topic: "Math" };
assignment.call(otherHomework); // Kyle says to study Math
相同的上下文感知函数以三种不同方式调用,每次this
引用不同的对象。
总结
理解JavaScript的这些核心概念对于成为高效开发者至关重要。迭代器提供了处理数据的标准化方式,闭包允许函数记住其词法环境,而this
关键字提供了动态执行上下文。掌握这些概念将帮助你编写更清晰、更灵活的代码。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考