都8月份了,还在面试找工作,找到合适的好难啊。今天问了js的作用域,以及闭包,原形链等等,回答的不好,还是复习一下吧~,希望也能帮助到你
作用域 是指程序中变量、函数和对象的可访问范围,它决定了代码中不同部分对变量的访问权限。简单来说,作用域回答了一个关键问题: "变量在哪里可以被访问到呢?"
作用域的存在本质是为了实现变量的隔离与访问控制,避免变量污染(不同部分的变量重名导致的冲突),同时优化内存使用。
1.2 作用域的分类(以 JavaScript 为例)
在 JavaScript 中,作用域主要分为以下几类,它们的特性和行为各有不同:
(1)全局作用域(Global Scope)
-
定义:在代码最外层声明的变量或函数,或未声明直接赋值的变量(不推荐),属于全局作用域。
-
特性:在程序的任何位置都能被访问,生命周期与程序一致(页面关闭或进程结束才销毁)。
-
示例:
jsconst globalVar = "我是全局变量"; // 全局作用域 function foo() { console.log(globalVar); // 可访问全局变量 } foo(); // 输出:"我是全局变量"
-
风险:过多全局变量会导致命名冲突,建议尽量减少全局变量的使用。
(2)函数作用域(Function Scope)
-
定义:在函数内部声明的变量,仅在当前函数内部可访问,外部无法直接访问。
-
特性:函数执行时创建,函数执行完毕后通常会被销毁(除非被闭包引用)。
-
示例:
jsfunction foo() { const funcVar = "我是函数内变量"; // 函数作用域 console.log(funcVar); // 输出:"我是函数内变量" } foo(); console.log(funcVar); // 报错:funcVar is not defined(外部无法访问)
(3)块级作用域(Block Scope,ES6 新增)
-
定义 :由
{}
包裹的代码块(如if
、for
、while
、switch
或单独的{}
)中,通过let
或const
声明的变量,仅在当前块内可访问。 -
特性 :解决了 ES5 中
var
声明的变量 "变量提升" 导致的块级无隔离问题。 -
示例:
jsif (true) { let blockVar = "我是块级变量"; // 块级作用域 console.log(blockVar); // 输出:"我是块级变量" } console.log(blockVar); // 报错:blockVar is not defined(块外无法访问) // 对比var的行为(无块级隔离) if (true) { var noBlockVar = "我没有块级隔离"; } console.log(noBlockVar); // 输出:"我没有块级隔离"(var声明的变量会"溢出"块级)
1.3 作用域链(Scope Chain):变量查找的 "路径"
当访问一个变量时,JavaScript 引擎会沿作用域链 依次查找:从当前作用域开始,逐层向外层作用域查找,直到全局作用域。若找到则使用该变量,若未找到则抛出ReferenceError
。
作用域链的形成规则:
- 每个作用域都有一个 "父级作用域"(全局作用域没有父级)。
- 作用域链由当前作用域及其所有父级作用域组成。
- 作用域链在函数定义时确定(词法作用域特性),而非执行时。
示例:作用域链的查找过程
js
const globalVar = "全局";
function outer() {
const outerVar = "外层";
function inner() {
const innerVar = "内层";
// 查找顺序:inner作用域 → outer作用域 → 全局作用域
console.log(innerVar); // 内层
console.log(outerVar); // 外层
console.log(globalVar); // 全局
}
inner();
}
outer();
1.4 词法作用域 vs 动态作用域
-
词法作用域(静态作用域) :作用域由代码定义时的位置决定(如 JavaScript、Java、Python 等)。函数的作用域在函数定义时就已确定,与执行位置无关。
-
动态作用域 :作用域由函数执行时的位置决定(如 Bash、Perl 的某些模式)。
示例:词法作用域的确定性
js
const value = "全局";
function foo() {
console.log(value); // 此处的value由定义时的作用域决定
}
function bar() {
const value = "局部";
foo(); // 执行foo(),但foo的作用域在定义时已绑定全局value
}
bar(); // 输出:"全局"(而非"局部")
二、闭包(Closure):作用域的 "留存机制"
2.1 闭包的定义与本质
闭包 是指函数能够访问其外部作用域(父级作用域)中的变量,即使外部函数已经执行完毕并退出。从技术角度讲,闭包是由函数及其关联的词法环境组合而成的实体。
闭包的形成条件:
- 存在嵌套函数(内部函数嵌套在外部函数中);
- 内部函数引用了外部函数的变量或参数;
- 内部函数被外部作用域引用或返回,使得内部函数能在外部函数执行完毕后继续存在。
2.2 闭包的工作原理
当外部函数执行时,会创建一个函数作用域(包含其变量、参数等)。正常情况下,外部函数执行完毕后,其作用域会被垃圾回收机制销毁。但如果内部函数引用了外部函数的变量,且内部函数被外部保留(如返回给全局),则外部函数的作用域会被 "保留",供内部函数后续访问。
示例:闭包的基本形态
js
function outer() {
const outerVar = "我是外部变量";
// 内部函数引用外部变量
function inner() {
console.log(outerVar); // 引用outer的变量
}
return inner; // 返回内部函数,使其能在outer外部执行
}
// outer执行完毕后,inner被保留在全局
const closureFunc = outer();
closureFunc(); // 输出:"我是外部变量"(此时outer已执行完毕,但作用域被闭包保留)
2.3 闭包的经典应用场景
闭包并非 "语法特性",而是作用域机制的自然结果,在实际开发中有广泛应用:
(1)数据封装与私有变量
JavaScript 没有原生的私有变量语法,但可通过闭包模拟私有变量,实现 "变量只能通过特定方法访问 / 修改" 的封装效果。
js
function createCounter() {
let count = 0; // 私有变量,外部无法直接访问
return {
increment: () => { count++; },
decrement: () => { count--; },
getCount: () => count
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount()); // 输出:2
console.log(counter.count); // 输出:undefined(无法直接访问私有变量)
(2)函数工厂:动态生成函数
通过闭包可以创建带有 "记忆" 能力的函数,根据参数动态生成不同逻辑的函数。
js
function createGreeting(prefix) {
// prefix被闭包保留
return function(name) {
console.log(`${prefix}, ${name}!`);
};
}
const sayHello = createGreeting("Hello");
const sayHi = createGreeting("Hi");
sayHello("Alice"); // 输出:"Hello, Alice!"
sayHi("Bob"); // 输出:"Hi, Bob!"
(3)防抖与节流
闭包可用于保存函数执行的状态(如定时器 ID、时间戳),实现防抖(debounce)和节流(throttle)功能。
js
// 防抖:多次触发仅最后一次生效
function debounce(fn, delay) {
let timer = null; // 闭包保留定时器ID
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// 示例:输入框搜索防抖
const searchInput = document.getElementById("search");
searchInput.addEventListener("input", debounce((e) => {
console.log("搜索:", e.target.value);
}, 500));
(4)模块化开发
在 ES6 模块普及前,闭包是实现模块化的核心方式,通过立即执行函数(IIFE)创建独立作用域,避免全局污染。
js
const module = (function() {
const privateVar = "私有变量";
function privateFunc() {
return privateVar;
}
// 暴露公共接口
return {
publicMethod: () => privateFunc()
};
})();
console.log(module.publicMethod()); // 输出:"私有变量"
console.log(module.privateVar); // 输出:undefined
2.4 闭包的内存管理与常见问题
闭包虽强大,但如果使用不当,可能导致内存泄漏或逻辑错误,需重点关注:
(1)内存泄漏风险
闭包会保留外部函数的作用域,若闭包被长期引用(如挂载在全局变量上),其引用的变量不会被垃圾回收,可能导致内存占用过高。
解决方式:
- 不再使用闭包时,手动解除引用(如设置为
null
); - 避免在闭包中引用过大的对象或不必要的变量。
(2)循环中的闭包陷阱(ES5 时代常见)
在 ES5 中,var
声明的变量没有块级作用域,循环中创建的闭包可能共享同一变量,导致结果不符合预期。
问题示例:
js
// 预期:依次输出0、1、2,实际输出3、3、3
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 所有闭包共享同一个i(循环结束后i=3)
}, 100);
}
解决方式:
-
使用
let
(块级作用域,每次循环创建独立变量); -
用立即执行函数(IIFE)为每个闭包绑定独立变量。
js// 方案1:使用let for (let i = 0; i < 3; i++) { setTimeout(function() { console.log(i); // 输出:0、1、2 }, 100); } // 方案2:IIFE(ES5兼容) for (var i = 0; i < 3; i++) { (function(j) { setTimeout(function() { console.log(j); // 输出:0、1、2 }, 100); })(i); }
(3)闭包中的 this 指向问题
闭包中的this
默认指向全局对象(非严格模式)或undefined
(严格模式),而非外部函数的this
,需注意绑定。
示例:
js
const obj = {
name: "obj",
outer: function() {
const self = this; // 保存外部this
return function inner() {
console.log(this.name); // 输出:undefined(this指向全局)
console.log(self.name); // 输出:"obj"(通过变量保存外部this)
};
}
};
const func = obj.outer();
func();
三、作用域与闭包的关联与总结
3.1 核心关联
- 闭包是作用域机制的延伸:没有作用域的嵌套和变量引用,就不会形成闭包;
- 作用域链是闭包的 "基础设施":闭包能访问外部变量,本质是通过作用域链查找;
- 词法作用域决定闭包的 "访问范围":闭包只能访问其定义时所处的外部作用域变量,与执行位置无关。
3.2 关键结论
- 作用域是变量的访问规则,决定了代码中变量的可见范围,分为全局、函数、块级作用域;
- 作用域链是变量查找的路径,由当前作用域及其父级作用域组成,在函数定义时确定;
- 闭包是函数与外部作用域的绑定关系,允许函数在外部执行时仍访问外部变量;
- 闭包的核心价值是状态留存与数据封装,但需注意内存管理,避免泄漏。
3.3 实践建议
- 合理使用块级作用域(
let
/const
)替代var
,减少作用域污染; - 利用闭包实现模块化和私有变量,但避免过度嵌套导致逻辑复杂;
- 长期保留的闭包需手动清理引用,防止内存泄漏;
- 理解作用域链的查找规则,避免变量遮蔽(内层变量覆盖外层变量)导致的 bugs。
看到这里的朋友,为你点赞,加油吧!