JS进修系列 03 - 函数作用域和块作用域

在 ES5 及之前版本,JavaScript 只具有函数作用域,没有块作用域(with 和 try…catch 除外)。在 ES6 中,JS 引入了块作用域,{ } 内是零丁的一个作用域。采纳 let 或许 const 声明的变量会挟持地点块的作用域,也就是说,这声明关键字会将变量绑定到地点的恣意作用域中(通常是 {…} 内部)。

本日,我们就来深切研究一下函数作用域块作用域

1. 函数中的作用域

函数作用域的寄义是指,属于这个函数的任何声明(变量或函数)都能够在这个函数的范围内运用及复用(包括这个函数嵌套内的作用域)。

举个例子:

function foo (a) {
   var b = 2;

   // something else

   function bar () {
      // something else   
   }

   var c = 3;
}

bar();      // 报错,ReferenceError: bar is not defined
console.log(a, b, c);        // 报错,缘由同上

在这段代码中,函数 foo 的作用域包括了标识符a、b、c 和 bar ,函数 bar 的作用域中又包括别的标识符。

由于标识符 a、b、c 和 bar都属于函数 foo 的作用域,所以在全局作用域中接见会报错,由于它们都没有定义,然则在函数 foo 内部,这些标识符都是能够接见的,这就是函数作用域。

1.1 为何要有这些作用域

当我们用作用域把代码包起来的时刻,实在就是对它们举行了“隐蔽”,让我们对其有控制权,想让谁接见就能够让谁接见,想制止接见也很轻易。

想像一下,假如一切的变量和函数都在全局作用域中,固然我们能够在内部的嵌套作用域中接见它们,然则由于暴露了太多的变量或函数,它们能够被故意或许无意的改动,以非预期的体式格局运用,这就致使我们的顺序会涌现林林总总的题目,严峻会致使数据泄漏,构成无法挽回的效果。

比方:

var obj = {
   a: 2,
   getA: function () {
      return this.a;
   }
};

obj.a = 4;
obj.getA();      // 4

这个例子中,我们能够恣意修正对象 obj 内部的值,在某种情况下这并非我们所希冀的,采纳函数作用域就能够处理这个题目,私有化变量 a 。

var obj = (function () {
  var a = 2;
  return {
     getA: function () {
        return a;
     },
     setA: function (val) {
        a = val;
     }
  }
}());

obj.a = 4;
obj.getA();      // 2
obj.setA(8);
obj.getA();      // 8

这里经由过程马上实行函数(IIFE)返回一个对象,只能经由过程对象内的要领对变量 a 举行操纵,实在这里有闭包的存在,这个我们在今后会深切议论。

“隐蔽”作用域中的变量和函数所带来的另一个优点,是能够防止同名标识符之间的争执,争执会致使变量的值被不测掩盖。

比方:

function foo () {
   function bar (a) {
      i = 3;        // 修正了 for 轮回所属作用域中的 i
      console.log(a + i);
   }

   for (var i = 0; i < 10; i++) {
      bar(i * 2);      // 这里由于 i 总会被设置为 3 ,致使无穷轮回
   }
}

foo();

bar(…) 内部的赋值表达式 i = 3 不测的掩盖了声明在 foo(…) 内部 for 轮回中的 i ,在这个例子中由于 i 一直被设置为 3 ,永久满足小于 10 这个前提,致使无穷轮回。

bar(…) 内部的赋值操纵须要声明一个当地变量来运用,采纳任何名字都能够,var i = 3; 就能够满足这个请求。别的一种要领是采纳一个完整差别的标识符称号,比方 var j = 3; 。然则软件设想在某种情况下能够自然而然的请求运用一样的标识符称号,因而在这类情况下运用作用域来“隐蔽”内部声明是唯一的最好挑选。

总结来讲,作用域能够起到两个作用:

  • 私有化变量或函数
  • 躲避同名争执
1.2 函数声明和函数表达式

假如 function 是声明中的第一个词,那末就是一个函数声明,不然就是一个函数表达式。

函数声明举个例子:

function foo () {
   // something else
}

这就是一个函数声明。

函数表达式分为匿名函数表达式和签字函数表达式。

关于函数表达式来讲,最熟习的场景能够就是回调参数了,比方:

setTimeout(function () {
   console.log("I wait for one second.")
}, 1000);

这个叫作匿名函数表达式,由于 function ()… 没有称号标识符。函数表达式能够是匿名的,然则函数声明不能够省略函数名,在 javascript 中这是不法的。

匿名函数表达式誊写轻便,然则它也有几个瑕玷须要注重:

  1. 匿名函数在浏览器栈追踪中不会显现出故意义的函数名,这会加大调试难度。
  2. 假如没有函数名,当函数须要援用本身的时刻就只能运用已不是规范的 arguments.callee 来援用,比方递归。在事宜触发后的事宜监听器中也有能够须要经由过程函数名来解绑本身。
  3. 匿名函数对代码的可读性和可理解性有肯定的影响。一个故意义的函数名能够让代码不言自明。

签字函数表达式又叫行内函数表达式,比方:

setTimeout(function timerHandler () {
   console.log("I wait for one second.")
}, 1000);

如许,在函数内部须要援用本身的时刻就能够经由过程函数名来援用,固然要注重,这个函数名只能在这个函数内部运用,在函数外运用时未定义的。

1.3 马上实行函数表达式(IIFE)

IIFE 全写是 Immediately Invoked Function Expression,马上实行函数。

var a = 2;

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

console.log(a);      // 2

由于函数被包括在一对 ( ) 括号内部,因而成为了一个函数表达式,经由过程在末端加上另一对 ( ) 括号能够马上实行这个函数,比方 (function () {})() 。第一个 ( ) 将函数变成函数表达式,第二个 ( ) 实行了这个函数。

也有别的一种马上实行函数的写法,(function () {}()) 也能够马上实行这个函数。

var a = 2;

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

console.log(a);      // 2

这两种写法功用是完整一样的,详细看人人运用。

IIFE 的另一种广泛的进阶用法是把它们当作函数挪用并通报参数进去。

var a = 2;

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

console.log(a);      // 2

我们将 window 对象的援用通报进去,但将参数命名为 global,因而在代码作风上对全局对象的援用变得比援用一个没有“全局”字样的变量越发清晰。固然能够从外部作用域通报你须要的任何东西,并将变量命名为任何你以为适宜的笔墨。这关于革新代码作风黑白常有协助的。

这个形式的别的一个运用场景是处理 undefined 标识符的默认值被毛病掩盖的异常(这并不罕见)。将一个参数命名为 undefined ,然则并不传入任何值,如许就能够保证在代码块中 undefined 的标识符的值就是 undefined 。

undefined = true;

(function IIFE (undefined) {
   var a;
   if (a === undefined) {
      console.log("Undefined is safe here.")
   }
}()); 

2. 块作用域

ES5 及之前 JavaScript 中具有块作用域的只要 with 和 try…catch 语句,在 ES6 及今后的版本添加了具有块作用域的变量标识符 let 和 const 。

2.1 with
var obj = {
   a: 2,
   b: 3
};

with (obj) {
   console.log(a);      // 2
   console.log(b);      // 3
}

console.log(a);      // 报错,a is not defined
console.log(b);      // 报错,a is not defined

用 with 从对象中建立出的作用域仅在 with 声明中而非外部作用域中有效。

2.2 try…catch
try {
  undefined();      // 不法操纵
} catch (err) {
  console.log(err);      // 一般实行
}

console.log(err);      // 报错,err is not defined

try/catch 中的 catch 分句会建立一个块作用域,个中的变量声明仅在 catch 内部有效。

2.3 let

let 关键字能够将变量绑定到恣意作用域中(通常是 {…} 内部)。换句话说,let 为其声明的变量隐式的挟制了地点的块作用域。

var foo = true;

if (foo) {
   let a = 2;
   var b = 2;
   console.log(a);      // 2
   console.log(b);      // 2
}

console.log(b);      // 2
console.log(a);      // 报错,a is not defined

用 let 将变量附加在一个已存在的块作用域上的行动是隐式的。在开辟和修正代码的过程当中,假如没有亲昵关注哪些代码块作用域中有绑定的变量,而且习惯性的挪动这些块或许将其包括到其他块中,就会致使代码杂沓。

为块作用域显现的建立块能够部分处理这个题目,使变量的隶属关联变得越发清晰。

var foo = true;

if (foo) {
   {
      let a = 2;
      console.log(a);      // 2
   }
}

在代码的恣意位置都能够运用 {…} 括号来为 let 建立一个用于绑定的块。

另有一点要注重的是,在运用 var 举行变量声明的时刻会存在变量提拔,提拔是指声明会被视为存在于其所涌现的作用域的全部范围内。然则运用 let 举行的声明不会存在作用域提拔,声明的变量在被运转之前,并不存在。

console.log(a);      // undefined
console.log(b);      // 报错, b is not defined

// 在浏览器中运转这段代码时,由于前面报错了,所以不会看到接下来打印的效果,然则理论上就是如许的效果
var a = 2;
console.log(a);      // 2 

let b = 4;
console.log(b);      // 4

2.3.1 渣滓网络
另一个块作用域异常有效的缘由和闭包及渣滓内存的接纳机制有关。
举个例子:

function processData (data) {
   // do something
}

var bigData = {...};

processData(bigData);

var btn = document.getElementById('my_button');

btn.addEventListener('click', function () {
   console.log('button clicked');
}, false);

这个按钮点击事宜的回调函数中并不须要 bigData 这个异常占内存的数据,理论上来讲,当 processData 函数处理完以后,这个占领大批空间的数据结构就能够被渣滓接纳了。然则,由于这个事宜回调函数构成了一个掩盖当前作用域的闭包,JavaScript 引擎极有能够依旧保留着这个数据结构(取决于详细完成)。

运用块作用域能够处理这个题目,能够让引擎清晰的晓得没有必要继承保留这个 bigData 。

function processData (data) {
   // do something
}

{
   let bigData = {...};

   processData(bigData);
}

var btn = document.getElementById('my_button');

btn.addEventListener('click', function () {
   console.log('button clicked');
}, false);

2.3.2 let 轮回
一个 let 能够发挥优势的典范例子就是 for 轮回。

var lists = document.getElementsByTagName('li');

for (let i = 0, length = lists.length; i < length; i++) {
   console.log(i);
   lists[i].onclick = function () {
     console.log(i);      // 点击每一个 li 元素的时刻,都是相对应的 i 值,而不像用 var 声明 i 的时刻,由于没有块作用域,所以在回调函数经由过程闭包查找 i 的时刻找到的都是末了的 i 值
   };
};

console.log(i);      // 报错,i is not defined

for 轮回头部的 let 不仅将 i 绑定到 fir 轮回的块中,事实上它将其从新绑定到了轮回的每一个迭代中,确保上一个轮回迭代结束时的值从新举行赋值。

固然,我们在 for 轮回中运用 var 时也能够经由过程马上实行函数构成一个新的闭包来处理这个题目。

var lists = document.getElementsByTagName('li');

for (var i = 0, length = lists.length; i < length; i++) {
   lists[i].onclick = (function (j) {
        return function () {
           console.log(j);
        }
   }(i));
}

或许

var lists = document.getElementsByTagName('li');

for (var i = 0, length = lists.length; i < length; i++) {
   (function (i) {
      lists[i].onclick = function () {
         console.log(i);
      }
   }(i));
}

实在道理不过就是,为每一个迭代建立新的闭包,马上实行函数实行完后原本应当烧毁变量,开释内存,然则由于这里有回调函数的存在,所以构成了闭包,然后经由过程形参举行同名变量掩盖,所以找到的 i 值就是每一个迭代新闭包中的形参 i 。

2.4 const

除了 let 之外,ES6 还引入了 const ,一样能够用来建立作用域变量,但其值是牢固的(常亮)。以后任何试图修正值的操纵都邑引发毛病。

var foo = true;

if (foo) {
   var a = 2;
   const b = 3;      // 包括在 if 中的块作用域常亮

   a = 3;      // 一般
   b = 4;      // 报错,TypeError: Assignment to constant variable
}

console.log(a);      // 3
console.log(b);      // 报错, b is not defined

和 let 一样,const 声明的变量也不存在“变量提拔”。

3. 总结

函数是 JavaScript 中最罕见的作用域单位。块作用域指的是变量和函数不仅能够属于所处的函数作用域,也能够属于某个代码块。

本质上,声明在一个函数内部的变量或函数会在所处的作用域中“隐蔽”起来,这是故意为之的优越软件的设想准绳。

有些人以为块作用域不应当完整作为函数作用域的替换计划。两种功用应当同时存在,开辟者能够而且也应当根据须要挑选运用哪一种作用域,制造可读、可保护的优秀代码。

迎接关注我的民众号

《JS进修系列 03 - 函数作用域和块作用域》

    原文作者:liuxuan
    原文地址: https://segmentfault.com/a/1190000008440514
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞