发布于 2024 年 2 月 26 日,星期一
JavaScript中的块级作用域和函数作用域是理解变量生命周期和可见性的关键。块级作用域由`let`和`const`关键字定义,变量仅在代码块内有效,避免全局污染。函数作用域由`var`定义,变量在整个函数内有效,可能导致意外的变量覆盖。块级作用域更符合现代编程习惯,减少错误,提高代码可维护性。函数作用域则依赖于函数封装,适用于传统编程模式。理解这两种作用域的本质差异,有助于编写更安全、更清晰的代码。
function foo(a) { var b = 2; function bar() { // ... } var c = 3;}
bar(); // ReferenceError: bar is not definedconsole.log(a, b, c); // 全都抛出 ReferenceError
属于这个函数的全部变量都可以在整个函数的范围内使用及复用(在嵌套的作用域中也可以使用)
。这种设计方案可根据需要改变值类型的 "动态" 特性。反过来
可带来一些启示:从所写的代码中挑选出一个任意片段,然后就用函数声明的方式对它进行包装,实际上就是把这些代码 "隐藏" 起来了。
实际的结果就是在这个代码片段的周围创建了一个新的作用域
,也就是说这段代码中的任何声明(变量或函数)都将绑定在这个新创建的函数作用域中,而不是先前所在的作用域中。换句话说,可把变量和函数包裹在一个函数的作用域中,然后用这个作用域来 "隐藏" 他们。function doSomething(a) { b = a + doSomethingElse( a * 2 ); console.log( b * 3 );}function doSomethingElse(a) { return a - 1;}var b;doSomething( 2 ); // 15
function doSomething(a) { function doSomethingElse(a) { return a - 1; } var b; b = a + doSomethingElse( a * 2 ); console.log( b * 3 );}doSomething( 2 ); // 15
"隐藏" 作用域中的变量和函数的另一个好处是可避免同名标识符的冲突
,两个标识符名字相同但用途不同,无意间可能会造成命名冲突,而冲突会导致变量的值被意外覆盖。function foo() { function bar(a) { i = 3; // 修改for 循环所属作用域中的i console.log( a + i ); } for (var i=0; i<10; i++) { bar( i * 2 ); // 糟糕,无限循环了! }}foo();
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ... }}
2. **模块管理**
var a = 2;function foo() { // <-- 添加这一行 var a = 3; console.log( a ); // 3} // <-- 以及这一行foo(); // <-- 以及这一行console.log( a ); // 2
var a = 2;(function foo() { // <-- 添加这一行 var a = 3; console.log(a); // 3})(); // <-- 以及这一行console.log(a); // 2
(function...
而不仅是以 function...
开始。函数会被当做函数表达式而不是一个标准的函数声明来处理。
function 关键字出现在声明中的位置
(不仅仅是一行代码,而是整个声明中的位置)。如果 function 为声明中的第一个关键字,那它就是一个函数声明,否则就是一个函数表达式。
函数声明和函数表达式之间最重要的区别就是他们的名称标识符将会绑定在何处。
(function foo(){...})
作为函数表达式意味着 foo
只能在 ...
所代表的位置中被访问,外部作用域则不行。setTimeout(function () { console.log("I waited 1 second!");}, 1000);
匿名函数表达式
,因为 function()..
没有名称标识符。函数表达式可以是匿名的,而函数声明则不可以省略函数名——在JavaScript 的语法中这是非法的。匿名函数在栈追踪中不会显示出有意义的函数名
,这使调试很困难。过期
的 arguments.callee
来引用。代码可读性
不是很友好。setTimeout(function timeoutHandler() { console.log("I waited 1 second!");}, 1000);
var a = 2;(function IIFE() { var a = 3; console.log(a); // 3})();console.log(a); // 2
var a = 2;(function IIFE() { var a = 3; console.log(a); // 3}());console.log(a); // 2
把他们当做函数调用并传递参数进去
,如下:var a = 2;(function IIFE(global) { var a = 3; console.log(a); // 3 console.log(global.a); // 2})(window);console.log(a); // 2
解决 undefined 标识符的默认值被错误覆盖导致的异常
。
undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!(function IIFE(undefined) { var a; if (a === undefined) { console.log("Undefined is safe here!"); }})();
倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去
。var a = 2;(function IIFE(def) { def(window);})(function def(global) { var a = 3; console.log(a); // 3 console.log(global.a); // 2});
for (var i = 0; i < 5; i++){ console.log(i);}
var foo = true;if(foo) { var bar = foo * 2; bar = something(bar); console.log(bar);}
用 with 从对象中创建出的作用域仅在 with 所处作用域中有效
。try/catch
的 catch 分句会创建一个块作用域,其中声明的变量仅会在 catch 内部有效。try { undefined(); // 目的是让他抛出一个异常} catch (error) { console.log("error ------>", error); // TypeError: undefined is not a function}console.log("error ------>", error); // ReferenceError: error is not defined
error 仅存在于 catch 分句内部,当视图从别处引用它时会抛出错误。
let 关键字将变量绑定到所处的任意作用域中(通常是 { ... } 内部)。换句话说,let 声明的变量隐式地了所在的块作用域。
var foo = true;if(foo) { var bar = foo * 2; bar = something(bar); console.log(bar);}console.log(bar); // ReferenceError: bar is not defined
使用 let 进行的声明不会再块作用域中进行提升。声明的代码被运行前,声明并不 "存在"。
{ console.log(bar); // ReferenceError let bar = 2;}
1. 垃圾收集
function process(data) { // do something}var someObj = {};process(someObj);var btn = document.getElementById('my_button');btn.addEventListener('click', function click(evt) { console.log('clicked');}, /*capturingPhase=*/false);
function process(data) { // do something}// 在这个块中定义内容就可以销毁了{ var someObj = {}; process(someObj);}var btn = document.getElementById('my_button');btn.addEventListener('click', function click(evt) { console.log('clicked');}, /*capturingPhase=*/false);
2. let循环
for(let i = 0; i < 10; i++) { console.log(i);};console.log(i); // ReferenceError
{ let i; for(i = 0; i < 10; i++) { let j = i; // 每次迭代中重新绑定 console.log(j); };}
var foo = true, baz = 10;if (foo) { var bar = 3; if (baz > bar) { console.log( baz ); } // ...}
var foo = true, baz = 10;if (foo) { var bar = 3; // ...}if (baz > bar) { console.log( baz );}
var foo = true, baz = 10;if (foo) { let bar = 3; if (baz > bar) { // <-- 移动代码时不要忘了 bar! console.log( baz ); }}
ES6 还引入了 const, 同样可用来创建块级作用域,但其值是固定的(常量), 不可修改。
var foo = true;if (foo) { var a = 2; const b = 3; // 包含在 if 中的块作用域常量 a = 3; // 正常 ! b = 4; // 错误 !}console.log( a ); // 3console.log( b ); // ReferenceError!
函数时 JavaScript 中最常见的作用域单元。
块作用域值的是变量和函数布局可以属于所处的作用域,也可以属于某个代码块(通常指 {...} 内部)
ES3
开始, try/catch 结构在 catch 分句中具有块作用域
。ES6
引入了 let,const 关键字来创建块级作用域
。