美文网首页浓缩解读前端系列书籍一周一章前端书
一周一章前端书·第3周:《你不知道的JavaScript(上)》

一周一章前端书·第3周:《你不知道的JavaScript(上)》

作者: 梁同学de自言自语 | 来源:发表于2017-10-11 21:02 被阅读16次

第3章:函数作用域和块级作用域

3.1 函数中的作用域

  • JavaScript变量的查找规则是由内到外的,阅读如下代码:
function foo(a){
    var b = 2;
    function bar(){
        //...
    }
    var c = 3;
}

bar();  //失败
console.log(a,b,c); //失败
  • foo()函数作用域声明了变量abc以及函数bar,如果在外部使用这些函数内的变量或函数都会失败。
  • 函数作用域的含义就是说,函数内的变量只允许在函数范围内使用(也包括在内部嵌套函数)。

3.2 隐藏内部实现

  • 既然函数中变量的活动范围只限制在函数内,反过来其实也可以这么理解:
  • 我们挑选一个代码片段,用函数作用域将其包装起来了,而 函数内的具体细节(变量)对外不可见,被“隐藏”起来了。
  • 这种做法就是 软件设计中的最小授权最小暴露原则
  • 如果所有变量和函数都声明到全局作用域中,可能会发生无法预知的情况:
var b;
function doSomething(a){
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}

function doSomethingElse(a){
    return a - 1;
}

doSomething(2); //15

通过函数作用域修改后,是不是更安心了:

function doSomething(a){
    var b;
    function doSomethingElse(a){
        return a - 1;
    }
    b = a + doSomethingElse(a * 2);
    console.log(b * 3);
}
doSomething(2); //15
  • 另外,通过作用域隐藏变量还有一个好处:可以避免因为同名造成的变量冲突。
function foo(){
    function bar(a){
        i = 3;
        console.log(a + i);
    }
    
    for(var i=0;i<10;i++){
        bar(i * 2);
    }
};

foo();  //因为调用的i是通一个,造成无限循环
  • 针对同名变量冲突,大概有两种处理方式:

(1) 全局命名空间

  • 将本来暴露在全局作用域的诸多变量,统一放到全局的某个对象中,这个对象看做一个命名空间。
  • jQuery采用的就是此类做法,将所有和jQuery关联的第三方插件,都放到jQuery对象下。

(2) 模块管理

  • 如同seaJSrequireJS等现代化的模块机制一样
  • 首先将JS第三方库统一注册到模块管理器中,当调用的方法需要某个库时,从模块管理器中将该库挑选出来,显式的注入到函数作用域里。
  • 这样不仅避免了全局作用域的污染,更实现了按需加载。

3.3 函数作用域

  • 上文说到,通过函数作用域来隐藏变量能避免一系列的问题,但这并不是最理想的:
    • 为了隐藏几个变量,我还得声明一个具名函数,那这个函数本身,不也在污染全局作用域吗?
    • 其次,声明了函数还得去调用才能运行起来。
  • 是否有可以 不用指定函数名,并且还能自动运行的函数 呢?答案是肯定的:
var a = 2;
(function foo(){
    var a = 3;
    console.log(a); //3
})();
console.log(a); //2 函数作用域对内部的a做了隐藏,不影响全局的作用域
  • 当以()中括号包含函数时,则是函数表达式,而不是标准的函数声明。
  • 不要对函数表达式感到陌生,其实早在定时函数setTimeout()我们就有用到过:
setTimeout(function(){
    console.log('I waited 1 second!');
},1000);
  • 定时函数中用到的叫匿名函数表达式,因为没有指定函数名。
  • 注意,函数表达式是可以匿名的,但函数声明则不允许匿名。
  • 使用函数表达式需要注意的几点:
    • 匿名函数不便于调试;
    • 当函数需要引用自身的时候比较困难,只能引用已经过期的arguments.callee
    • 由于匿名函数缺乏函数名,代码的可读性较差;
  • ()包含一个函数可以构造一个函数表达式,而 在末尾紧跟着一个()可以立即执行这个函数
  • JS社区将它命名为 IIFE(Immediately Invoked Function Expresion),代表立即执行的函数表达式
  • IIFE还有另外一种写法:
(function(){
    console.log('test');
}())
  • IIFE可以传递参数
var a = 2;
(function(global){
    var a = 3;
    console.log(a); //3
    console.log(global.a);  //2
})(window);
console.log(a); //2
  • IIFE 还有一种 可以倒置代码的运行顺序 的玩法,这种模式在UMD(Universal Module Definition)项目中运用广泛:
var a = 2;
(function iife(def){
    def(window);
})(function(global){
    var a = 3;
    console.log(a); //3
    console.log(global.a);  //2
});

3.3 块作用域

  • 虽然函数作用域是最常见的作用域,但JavaScript也有其他的作用域单元,甚至在很多场景下,甚至用这些非函数的作用域单元实现功能,代码会更简洁、更优雅。
//虽然i声明于for循环的花括号范围内
//但注意i是用var来声明的,实际上它是全局作用域下的变量
for (var i=0;i<10;i++){
    consooe.log(i);
}
  • for循环中的i经常会由于忽略,会被绑定到全局作用域,而JavaScript的块级作用域就可以将变量限制在花括号的范围内。

with

  • 前文提到的with就可以构造块级作用域,但不提倡使用。

try/catch

  • catch分句中,也会创建一个块级作用域。在其中声明的变量仅在catch内部有效。
try{
    makeError();
}catch(err){
    console.log(err);
}
console.log(err);   //ReferenceError: err not found

let

  • ES6引入了 let关键字,可以将变量绑定到所在代码的作用域中 (通常是花括号{}内)。换句话说,let为其声明的变量隐式的劫持了所在的块作用域。
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;
}

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!

3.5 小结

  • 函数是JavaScript中最常见的作用域单位,声明在函数内的变量会被函数作用域“隐藏”起来,这是软件设计的最小暴露原则。
  • 函数不是唯一的作用域单位,块级作用域是指变量属于某个代码段(通常拥有花括号{})下。
  • try/catchcatch分句拥有块作用域。
  • ES6的letconst关键字能在任意代码段中创建块作用域变量。

相关文章

网友评论

    本文标题:一周一章前端书·第3周:《你不知道的JavaScript(上)》

    本文链接:https://www.haomeiwen.com/subject/vjdgyxtx.html