《你不知道的JavaScript》-- 精读(三)

308 阅读6分钟

知识点

1.函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)。

function foo(a){
    var b = 2;
    // 一些代码
    function bar(){
        // ...
    }
    // 更多的代码
    var c = 3;
}
bar(); // 失败
console.log(a,b,c); // 三个全都失败

2.隐藏内部实现

可以把变量和函数包裹在一个函数的作用域中,然后用这个作用域来“隐藏”它们。

为什么“隐藏”变量和函数是一个有用的技术?

最小特权原则:在软件设计中应该最小限度地暴露必要内容,而将其它内容都“隐藏”起来,比如某个模块或对象的API设计。

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

3.规避冲突

“隐藏”作用域中的变量和函数所带来的另一个好处,是可以避免同名标识符之间的冲突。

function foo(){
    function bar(a){
        // var i = 3;
        i = 3; // 修改for循环所属作用域中的i
        console.log(a+i);
    }
    for(var i = 0; i < 10; i++){
        bar(i * 2); // 糟糕,无限循环了!
    }
}

可以使用var i = 3修改上面的代码,得到正确的结果。在这种情况下使用作用域来“隐藏”内部声明是最佳选择。

3.1 全局命名空间

变量冲突的一个典型例子存在于全局作用域中。当程序中加载了多个第三方库时,如果它们没有妥善地将内部私有的函数或变量隐藏起来,就会很容易引发冲突。

这些库通常会在全局作用域中声明一个名字足够独特的变量,通常是一个对象,被用作命名空间,所有需要暴露给外界的功能都是该对象的属性。

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

3.2 模块管理

另外一种避免冲突的方法和现代的模块机制很接近,就是从众多模块管理器中挑选一个来使用。

4.函数作用域

在任意代码片段外部添加包装函数,可以将内部的变量和函数定义“隐藏”起来,外部作用域无法访问包装函数内部的任何内容。

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

这样会导致的问题是,首先必须声明一个具名函数foo(),意味着foo这个名称会“污染”所在作用域(在这个例子中是全局作用域)。其次,必须显示地通过函数名(foo())来调用才能运行。

JavaScript提供了能够同时解决函数不需要函数名,并且能够自动运行的方案。

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

函数声明和函数表达式的区别是function是否是声明的的第一个词,如果是就是函数声明。

(function foo(){..})作为函数表达式意味着foo只能在..所代表的位置中被访问。外部作用域则不行。foo变量被隐藏在自身中意味着不会非必要地污染外部作用域。

5.立即执行函数表达式(IIFE)

始终给函数表达式命名是一个最佳实践。

setTimeout(function timeoutHandler(){ // 快看,我有名字了!
    console.log("I waited 1 second");
},1000)

(function(){...})(),第一个()将函数变成表达式,第二个()执行了这个函数。

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的另一种用途是倒置代码的运行顺序。

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

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

6.块作用域

6.1 let

let 关键字可以将变量绑定到所在的任意作用域中(通常是{..}内部)。let为其声明的变量隐式地劫持了所在的块作用域。

var foo = true;
if(foo){
    let bar = foo * 2;
    bar = something(bar);
    console.log(bar);
}
console.log(bar); // ReferenceError
var foo = true;
if(foo){
    { // 显式的块
        let bar = foo * 2;
        bar = something(bar);
        console.log(bar);
    }
}
console.log(bar); // ReferenceError

let 进行的声明不会在块作用域中进行提升。

{
    console.log(bar); // ReferenceError
    let bar = 2; 
}

6.2 const

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

总结

函数是JavaScript中最常见的作用域单元。本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐藏”起来,这是有意为之的良好的软件设计原则。

但函数不是唯一的作用域单元。块作用域指的是变量和函数不仅可以属于所处的作用域,也可以属于某个代码块(通常指{...}内部)。

巴拉巴拉

关于标签

一朋友在我对自己产生严重质疑的时候对我说,不要轻易给自己加标签,当然也不要轻易给别人下定义。聊这个事情的时候,并没有醍醐灌顶,倒是最近经历的一些事,让我对他说的话,有了一些不一样的感悟。比如,你某件事没有做好,你的上级可能会觉得你没做好的原因是不够认真,经验不足,能力不够等,一旦他把其中的某个词加在你身上,你的第一反应可能他是对的,我就是这样的,甚至还会自我加注更多类似的。我想说这样是不对的,因为你可能无意中就多了一个困扰自己的负面标签。正确的做法应该是,他也许是对的,我还有需要提升,改正的地方,尽量避免正面的去同意他的话,而是从积极的角度说可以改正,提升的地方,很多事,是没有绝对的对错和好坏的,一个原则是,尽量不要给自己加注负面的具体的标签,因为会给人更深的印象,让人永远记住黑历史。