JavaScript

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > JavaScript块级函数作用域

JavaScript中块级作用域与函数作用域深入剖析

作者:控心crazy

这篇文章主要为大家介绍了JavaScript中块级作用域与函数作用域的实现原理深入剖析,

面试官必问系列:深入理解JavaScript块和函数作用域

在 JavaScript 中,究竟是什么会生成一个新的作用域,只有函数才会生成新的作用域吗?那 JavaScript 其他结构能生成新的作用域吗?

函数中的作用域

在之前的词法作用域中可见 JavaScript 具有基于函数的作用域,这也就意味着一个函数都会创建一个新的作用域。但其实并不完全正确,看以下例子:

function foo(a) {
var b = 2;
function bar() {
    // ...
}
var c = 3;
}

由于标识符 a, b, c 和 bar 都附属于 foo() 的作用域内,因此无法从 foo() 的外部对它们进行访问。也就是说,这些标识符在全局作用域中是无法被访问到的,因此如下代码会抛出 ReferenceError:

bar(); // ReferenceError: bar is not defined
console.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();

bar(...) 内部的赋值表达式 i = 3 意外地覆盖了声明在 foo(..) 内部 for 循环中的 i。在这个例子中将会导致无限循环,因为 i 被固定设置为 3,永远满足小于 10 这个条件。

规则冲突的方式

全局命名空间:在全局作用域中声明一个足够独特的变量,通常为一个对象,如下:

var MyReallyCoolLibrary = {
awesome: "stuff",
doSomething: function() {
    // ...
},
doAnotherThing: function() {
    // ...
}
}

模块管理

函数作用域

如下:

var a = 2;
function foo() { // <-- 添加这一行
var a = 3;
console.log( a ); // 3
} // <-- 以及这一行
foo(); // <-- 以及这一行
console.log( a ); // 2

JavaScript 提供了两种方案来解决:

var a = 2;
(function foo() {
// <-- 添加这一行
var a = 3;
console.log(a); // 3
})(); // <-- 以及这一行
console.log(a); // 2

匿名和具名

对于函数表达式最熟悉的就是回调参数了,如下:

setTimeout(function () {
console.log("I waited 1 second!");
}, 1000);

上述代码的改造结果:

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

IIFE 的应用场景

除了上述传统的 IIFE 方式,还有另一个方式,如下:

var a = 2;
(function IIFE() {
var a = 3;
console.log(a); // 3
}());
console.log(a); // 2

第一种形式中函数表达式被包含在 ( ) 中,然后在后面用另一个 () 括号来调用。第二种形式中用来调用的 () 括号被移进了用来包装的 ( ) 括号中。

这两种方式的选择全凭个人喜好。

IIFE 还有一种进阶用法,就是把他们当做函数调用并传递参数进去,如下:

var a = 2;
(function IIFE(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
})(window);
console.log(a); // 2

IIFE 的另一个应用场景是解决 undefined 标识符的默认值被错误覆盖导致的异常。

IIFE 的另一种变化的用途是倒置代码的运行顺序,将需要运行的函数放在第二位,在IIFE执行之后当做参数传递进去。

var a = 2;
(function IIFE(def) {
def(window);
})(function def(global) {
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
});

函数表达式 def 定义在片段的第二部分,然后当做参数(这个参数也叫做 def)被传递 IIFE 函数定义的第一部分中。最后,参数 def(也就是传递进去的函数)被调用,并将 window 传入当做 global 参数的值。

块作用域

将一个参数命名为 undefined, 但在对应的位置不传入任何值,这样就可以就保证在代码块中 undefined 标识符的值为 undefined

undefined = true; // 给其他代码挖了一个大坑!绝对不要这样做!
(function IIFE(undefined) {
var a;
if (a === undefined) {
    console.log("Undefined is safe here!");
}
})();

如下:

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

try/catch

很少有人注意,JavaScript 在 ES3 规范 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

let

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 );
}
}

const

ES6 还引入了 const, 同样可用来创建块级作用域,但其值是固定的(常量), 不可修改。

var foo = true;
if (foo) {
var a = 2;
const b = 3; // 包含在 if 中的块作用域常量
a = 3; // 正常 !
b = 4; // 错误 !
}
console.log( a ); // 3
console.log( b ); // ReferenceError!

小结

以上就是JavaScript中块级作用域与函数作用域的详细内容,更多关于JavaScript块级函数作用域的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文