块绑定如何工作

前言:块绑定

在传统意义上,变量声明工作的方式在Js一直是棘手的编程部分,在大多数基于C语言的编程语言中,变量(或绑定)被创造在声明出现的地方,然而在Js中情况并不是这样的。在Js中,你的变量实际上被创造依赖于你怎样声明它们,并且在ECMAScript6中提供了更容易控制作用域的选择。这一章阐述了为什么经典的var声明是易混淆的,介绍了在ECMAScript6中的块级绑定,并且提供了一些特别好的使用它们的实例。


var 声明和提升

用var声明的变量无论真实的声明出现在函数的哪个地方都被处理好像它们处于函数的顶部(或者在全局作用域,如果它是定义在一个函数的外面),这被称为提升,对于提升的作用的实例,思考下面这个函数定义:
function getValue(condition){ if(condition){ var value = "blue"; //other code return value; } else{ //value exsits here with a value of undefined return null; } //value exsits here with a value of undefined }
如果你不熟悉Js,你可能会期望变量 value仅仅在condition这个条件为真的情况下被创建。而事实上,变量value是无论如何都会被创建。在这个函数情形下,Javascript引擎改变了getValue函数,像下面这样:
function getValue(condition) { var value; if(condition){ value = "blue"; // other code return code; } else{ return null; } }
value的声明被提升到顶部,然而初始化仍然在相同的地方。这意味着变量value实际上在else子句中仍然是可访问的。如果从else子句访问,由于未初始化,变量将仅仅有一个undefined的值。
这常常花费新的Js开发者一些时间去适应声明提升,并且常常误解独特的行为会最终导致bugs.为此,ECMAScript6提出了块级作用域选项去更好地控制 一个变量的生命周期。

块级声明

块级声明是指在函数中声明的变量在给定块级范围外是不可访问的。块级作用域也称为词作用域,被这样创建:
1.在一个函数的内部
2.在一个块的内部(被字符 {} 标识)

块级作用域是许多基于C语言的编程语言的工作方式,并且在ECMAScript6中块级声明的提出旨在为Javascript提出相同的灵活性(和一致性)。

Let 声明

let 的声明语法和 var 的语法相同。你基本上能用 let 替换 var 去声明一个变量, 但是限制变量的作用域仅仅在当前代码块(有一些其他微妙的差异也会在之后讨论)。由于 let 声明不被提升在封闭块的顶部,你可能总是想放 let 声明在封闭块的最开始位置,以便使它们是可用的在整个块内是可访问的。下面是个例子:
function getValue(condition) { if (condition) { let value = "blue"; // other code return value; } else { //value doesn't exist return null; } // value does't exist }
这个版本的 getValue 函数的运作更接近于你所期望的它在基于C的编程语言中的实现。因为变量 value 被声明用 let 而不是 var,这个声明将不会提升到函数定义的顶部,并且一旦函数执行到 if 块的外面,变量 value 将不再是可访问的, 如果 condition 条件为 false,那么 value 将从不会声明和初始化。

无重复声明

如果一个标识符在一个作用域中早已被定义,然后在这个作用域中用 let 声明这个标识符会造成一个错误抛出。举例:
var count = 30; // Syntax error let count = 40;
在这个例子中,count 被声明两次:一次用 var ,一次用 let。因为 let 将不会重新定义一个已经在相同作用域内存在的标识符,所以let 声明将抛出一个错误。另一方面,如果 一个 let 声明在变量的包含作用域内以同样的名字创建一个新的变量将没有错误抛出,如下面代码所述:
var count = 30; //Does not throw an error if (condition) { let count = 40; //more code }
这个 let 声明没有抛出错误是因为它是在 if 语句范围中创建了一个名为 count 的新的变量,而不是在外围代码块中创建的。在 if 代码块内部,这个新的变量覆盖了全局变量 count ,阻止访问这个全局变量直到执行流离开这个 if 语句这个代码块 。

常量声明

在ECMAScript6中你也能用 const 声明语法定义一个变量。用 const 声明的变量被当做一个常量,这意味着它们的值一旦被设定将不能改变。为此,每一个 const 变量必须在声明的时候进行初始化,如下所示:
//Valid constant const maxItems = 30; //Syntax error: missing initialization const name;
因为maxItem变量被初始化了,所以它的 const 声明应该没有问题地工作。然而如果你尝试去运行包含这个代码的程序, name 变量将造成一个语法错误,这是因为 name 变量没有被初始化。
常量声明 vs Let声明
常量声明,像 let 声明,是块级声明。这意味着一旦执行流运行到常量被声明的代码块的外面时,常量将不再是可访问的,并且声明不被提升,如下所示:
if (condition) { const maxItem = 5; // more code } // maxItem isn't accessible here
在这段代码中,常量 maxItemif语句中被声明。一旦这个语句结束执行,在代码块的外面 maxItem 将不再是可访问的。
另一个和 let 相似的地方是:当 const 声明一个在相同的作用域内早已被定义的变量会抛出一个错误。如果这个变量用 var 声明(对于全局作用域或函数作用域)或者用 let 声明(对于块作用域内部),则它是无关紧要的。举例,思考下面代码:
var message = "hello!" let age = 25; // Each of these would throw an error. const message = ''Goodbye!'; const age = 30;
这两个 const 声明单独来说是有效的,但是鉴于在这个事件中前面的 varlet 声明,这两个声明都将如预期所示不工作。
尽管存在这些相同点,但是 letconst 之间有一个很大的不同之处需要牢记。在所有严格和非严格模式下企图给一个先前定义过的 const 常量赋值将抛出一个错误,:
const maxItem = 5; maxItem = 6; //throw error
在一个方面很像在其它语言中的常量,maxItem 变量之后不会被赋新值。然而,在另一方面不像在其他语言中的常量,如果一个常量的值是一个对象可能会被修改。
用 const 声明对象
一个 const 声明防止绑定的修改和不是它本来的的值。那意味着 const 对于对象的声明不会阻止那些对象的修改。举例:
const person = { name: "Nicholas" }; // works person.name = "Greg"; //throws an error person = { name: "Greg" }
在被绑定的 person 被创建用一个对象属性的初始化值。它是可能的去改变 person.name 没有导致错误,这是因为它只是去改变 person 所包含的属性并且并没有改变被绑定的 person 。当这段代码试图赋予一个值给 person(因此尝试去改变这个绑定),一个错误将被抛出。这个 const 如何工作的微妙处是容易被误解的。只需要记住: const 防止绑定的修改,不是防止绑定的对象的属性值的修改。

时域死区

一个用 letconst 声明的变量不能被访问直到这个变量被声明后。尝试这样做将会造成引用错误,甚至当正常地使用安全操作比如在这个例子中使用 typeof 操作符也会造成错误:
if (condition) { console.log(typeof value); //ReferenceError let value = "blue"; }
这里,变量 valuelet声明和初始化,但是这个语句从不执行因为前一行抛出了错误。这个问题在Javascript社区被称为时间死区。时间死区在ECMAScript规范中未被明确地命名,但是这个术语被用来描述为什么 let变量 和 const 变量不是可访问在它们未被声明前。这部分涉及了一些时间死区造成的声明位置的微妙处,并且尽管例子展示全部使用了 let 声明,注意同样的信息适用于 const
当Javascript引擎浏览一个即将执行的代码块并且发现一个变量声明,它要么提升这个声明到函数或全局作用域的顶部(对于 var),要么放这个声明到时间死区(对于 letconst).任何尝试在时间死区访问一个变量都会造成运行时错误。一旦执行流进行到变量声明的位置,那个变量才被移除时间死区因而可以安全使用。
这是正确的做法当你尝试去使用用 letconst 声明的变量在它并未被定义前。像之前的例子所阐述的,这个甚至正常地应用在安全操作符 typeof .然而你能用 typeof 在一个变量被声明的封闭块的外面去检测这个变量的类型,尽管它可能不会给你之后声明的这个变量的结构。思考这个代码:
console.log(typeof value); // "undefined" if (condition) { let value = 'blue"; }
typeof 操作符执行时这个变量 value 不是在时间死区,因为它出现在变量 value 被声明的封闭块的外面。这意味着没有变量绑定,并且 typeof 操作符简单地返回 undefined
时间死区仅仅是块绑定的一个独特的地方。另一个不得不说的独特的地方在它们在循环中的用法。

在循环中的块绑定

也许开发者最想让变量的块级作用域存在的地方是在 for 循环中,在这种情形下,计数器变量意味着只被在循环中使用。举例来说,它是非常普遍的在JavaScript中这样的代码:
for (var i = 0; i<10; i++){ process(item[i]); } // i is still accessible here console.log(i); //10
在其他语言中,默认是块级作用域,这个例子按预期的工作,并且仅仅对于for 循环可以访问到变量 i 。然而在JavaScript中,变量 i在循环被完成之后仍然是可访问的,因为 var 变量获得提升。相反用 let,在下面的代码中应该得到这样的结果:
for (let i = 0; i < 10; i++ ) { process(items[i]); } // i is not accessible here --throw an error console.log(i);
在这个例子中,变量 i 仅仅在 for 循环中存在,一旦循环完成,这个变量任何其他地方不再是可访问的。

在循环中的函数

var 的特点长期以来造成了在循环内创建函数的问题,因为循环变量在循环体作用域的外面是可访问的思考下面这个例子:
var funcs = []; for (var i = 1; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.fotEach(function(func) { func(); // output the number "10" ten times });
你可能通常希望这段代码输出数字0-9,但是它输出的是数字10十次在一行。那是因为 i 在每一次的循环迭代中是共享的,意味着在循环内创建的方法总是持有对同一变量的引用。这个变量 i 一旦循环结束则拥有值10,因而当console.log被执行时,在循环中每次值10被打印。
为了去修复这个问题,开发者在循环中用立即调用函数表达式(IIFE)去强制创建一个他们想要循环访问的新的变量副本 ,如下所示:
var funcs = []; for (var i = 0; i < 10; i++) { funcs.push(function(value) { return function() {console.log(value);} }(i)); } funcs.forEach(function(func) { func(); });
这个版本在循环的内部用一个IIFE,i 变量被传递给立即执行函数,在立即执行函数中创建它的副本并且以value 变量存储它。这是使用那个迭代函数的意义,因此每次调用函数返回了预期的循环计数从0到9的值。幸运的是,用ECMAScript6中的 letconst 的块级绑定对于你来说可以简化这个循环。

在循环中的Let 声明

一个 let 声明通过有效地模仿IIFE在上一个例子中所做的简化了循环。在每次迭代中,这个循环创建了一个新的变量并且初始化变量的值用上一次迭代所使用的的相同名字。那意味着你能完全省略IIFE并且获得你所期望的结果,像这样:
var funcs = []; for (let i = 0; i < 10; i++) { funcs.push(function() { console.log(i); }); } funcs.forEach(function(func) { func(); // outputs 0, then 1, then 2, up to 9 })
这个循环的实现的确像使用 var 和IIFE的循环,但是可以说更简洁。let 声明通过循环每次创建了一个新的变量 i,因此在循环体中创建的每个方法 获得了它自己i 的副本。每个 i 的副本有它在循环的迭代开始(即它被创建的那个迭代)被分配的值。对于for-infor-of 是同样的道理,如这儿所示:
var funcs = [], object = { a: true, b: true, c: true }; for (let key in object) { funcs.push(function() { console.log(key); }); } funcs.forEach(function(func) { func(); // outputs "a", then "b", then "c" });

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,565评论 6 479
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,021评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,003评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,015评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,020评论 5 370
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,856评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,178评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,824评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,264评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,788评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,913评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,535评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,130评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,102评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,334评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,298评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,622评论 2 343

推荐阅读更多精彩内容