执行上下文和作用域
摘要(总结):
- javascript 语言层面只原生支持两种作用域类型:全局作用域 和 函数作用域 。全局作用域程序运行就有,函数作用域只有定义函数的时候才有,它们之间是包含的关系。
- 作用域之间是可以嵌套的,我们把这种嵌套关系称为作用域链。
- 可执行代码在作用域中查询变量时,只能查询 本地作用域 及 上层作用域,不能查找内部的函数作用域。JS 引擎搜索变量时,会先询问本地作用域,找到即返回,找不到再去询问上层作用域...层层往上,直到全局作用域。
- javascript 中使用的是 “词法作用域”(静态作用域),因此函数作用域的范围在函数定义时就已经被确定,和函数在哪执行没有关系。
- 有权访问另一个函数内部变量的函数,我们称为 闭包。闭包的本质是利用了作用域的机制,来达到外部作用域访问内部作用域的目的。
- 闭包的使用场景非常广泛,然而过度使用会导致闭包内的变量所占用的内存空间无法释放,带来内 存泄露的问题。
- 我们可以借助于 chrome 开发者工具查找代码中导致了内存泄露的代码。
- 避免内存泄露的几种方法:避免使用全局变量、谨慎地为 DOM 绑定事件、避免过度使用闭包。最重要的,还是代码规范。
执行语句前创建的环境可以这样类比:
执行上下文
var 和 function 声明创建的变量和函数在全局对象中(浏览器环境是 window 对象),而 let,const,class 声明的变量创建在全局 scope 中。
let a = 'a';
console.log(a);
console.log(window.a); //没有
var a = 'a';
console.log(a);
console.log(window.a); //a
创建全局执行上下文:
暂时性死区:
var a = '123';
function foo() {
console.log(a);
let a;
}
foo(); //Uncaught ReferenceError: Cannot access 'a' before initialization
作用域
作用域:当前执行上下文或当前上下文的词法环境。
- 全局作用域: 全局执行上下文。全局作用域为程序的最外层作用域,一直存在。
- 函数作用域: 函数执行上下文。只有函数被定义时才会创建,包含在父级函数作用域 / 全局作用域内。
函数调用时的执行上下文: 函数在哪里创建,他就处于哪个执行上下文。
函数的作用域在函数创建时决定(词法作用域):
var a = 2;
function foo() {
console.log(a);
}
function bar() {
var a = 3;
foo();
}
bar(); //2 执行的foo 作用域在全局,则a 为全局的a
词法作用域(Lexical Scopes)是 javascript 中使用的作用域类型,词法作用域 也可以被叫做 静态作用域,与之相对的还有 动态作用域。 意味着函数被定义的时候,它的作用域就已经确定了,和拿到哪里执行没有关系,因此词法作用域也被称为 “静态作用域”。
作用域嵌套
用图片表示,上述代码一共有三层作用域嵌套,分别是:
- 全局作用域
- foo 作用域
- bar 作用域
当可执行代码内部访问变量时,会先查找本地作用域,如果找到目标变量即返回,否则会去父级作用域继续查找...一直找到全局作用域。我们把这种作用域的嵌套机制,称为 “作用域链”。
块级作用域 ES6
在 ES5 及之前并没有块级作用域的概念。
为什么需要块级作用域?
- 内层变量可能覆盖外层变量
- 用来计数的循环变量泄露为全局变量
if (true) {
var a = 1;
}
console.log(a); // 结果??? 1
ES6 使用 let 和 const 代替 var 关键字,和它最近的就会创建块级作用域。
if (true) {
let a = 1;
}
console.log(a); // ReferenceError
ES6 新增的 let 关键字跟 var 很相似,但它的作用域是块级的。块级作用域由最近的一对包含花括号界定。换句话说,if 块、while 块、function 块,甚至连单独的块也是 let 声明变量的作用域。
if (true) {
let a;
}
console.log(a); // ReferenceError: a没有定义
while (true) {
let b;
}
console.log(b); // ReferenceError: b没有定义
function foo() {
let c;
}
console.log(c); // ReferenceError: c没有定义
// 这没什么可奇怪的,var声明也会导致报错
// 这不是对象字面量,而是一个独立的块
// JavaScript解释器会根据其中内容识别出它来
{
let d;
} //单独的块也是 let 声明变量的作用域。
console.log(d); // ReferenceError: d没有定义
IIFE
ES5 使用 IIFE 可以模拟块级作用域,即在一个函数表达式内部声明变量,然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。
// module1.js
(function () {
//内嵌块级作用域
var a = 1;
console.log(a);
})();
// module2.js
(function () {
var a = 2;
console.log(a);
})();
模拟块级作用域锁定值(闭包+IIFE):
let divs = document.querySelectorAll('div');
for (var i = 0; i < divs.length; i++) {
divs[i].addEventListener(
'click',
(function (frozenCounter) {
return function () {
console.log(frozenCounter);
};
})(i),
);
}
延申: 闭包 - JavaScript | MDN 提供了更多解决遍历问题的方式。可以使用 forEach
通过 IIFE 实现 UMD 模块化
- 使用 IIFE 完成封装,解决了模块名污染全局作用域的问题。
- 赋予选择的权力,根据不同的环境条件选择不同的执行
// UMD 模块化
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery);
}
})(this, function ($) {
// methods
function myFunc() {}
// exposed public method
return myFunc;
});
如何创建/改变作用域
- 定义一个函数,创建函数作用域
function foo() {}
- 创建一个块级作用域,使用 let/const
// 内嵌块级作用域
{
let i;
for (i = 0; i < 5; i++) {
console.log(i);
}
}
console.log(i); // 抛出错误
// 循环的块级作用域
for (let i = 0; i < 5; i++) {
console.log(i);
}
console.log(i); // 抛出错误
- 使用 eval/with 修改作用域,不推荐。
作用域的应用场景
模块化。 面试官:说说作用域和闭包吧 - 掘金
一些问题
对象里写函数,对象的 fn 属性指向这个函数,但是函数并不属于这个对象,相对独立。
这个函数是全局的,对象不是语句块,不会产生作用域。
var obj = {
val: 3,
fn: function () {
return this.val;
},
};
obj.fn(); //this=>obj, 3
var,let,const
let 与 var
都是用于声明变量。
-
var 声明的变量会被提升到当前所在作用域最前面,但不赋值,到赋值的行才会赋值。
let 在 JavaScript 运行时中也会被提升,但由于“暂时性死区”(temporal dead zone)的缘故,实际上不能在声明之前使用 let 变量。因此,let 的提升跟 var 是不一样的。
// TDZ 块级作用域中也不能使用外部作用域的同名变量
var a = 8;
{
console.log(a); //ReferenceError: Cannot access 'a' before initialization
let a = 9;
}