EcmaScript 6 - 块级作用域(block scope)

1. EcmaScript 5作用域

EcmaScript5的作用域有全局作用域(global scope)与函数作用域(function scope)两种。

1.1 全局作用域

在全局作用域中定义的变量,在整个上下文中都是可以访问的。

var msg = 'Hello world';
console.log(msg); // Hello world
function sayHi(){
    console.log(msg);// Hello world
}
sayHi();

上面例子中msgsayHi()函数内外都可以访问。
在NodeJs中,在js文件中直接使用var关键字声明的变量,将会在模块上声明。
在浏览器中,在script标签中直接使用var关键字声明的变量,将定义在全局变量window上,window对象中的属性拥有全局作用域。

在严格模式(strict mode)下,变量在初始化前必须声明,否则会抛出ReferenceError

'use strict';
a = 2;
console.log(a); // Uncaught ReferenceError: a is not defined

在非严格模式下,使用未声明的变量将隐式声明为全局变量,定义在window上

a = 2;
console.log(a); // 2
console.log(a === window.a) // true

1.2 函数作用域

在函数作用域中定义的变量,只能在函数中被访问

function fn(){
    var a = 1;
    console.log(a); // 1
}
console.log(a); // Uncaught ReferenceError: a is not defined

2. 声明提升

声明提升(hoisting)指通过var关键字声明的变量,将提升到函数(或者全局作用域)的顶部进行声明,与声明语句的实际位置无关。

function sayHi(condition){
    if(condition){
        var msg = 'Hello world';
        console.log(msg); // Hello world
    }else{
        console.log(msg); // undefined
    }
}

sayHi(true);
sayHi(false);

上面例子中msgif语句中进行了声明和初始化赋值,但实际上msg将“提升”到函数顶部进行声明,而赋值的位置不变,还在if语句中。因此在else语句中同样可以访问msg变量,其值为undefinedJavascript引擎会将上面的代码处理成类似下面的样子。

function sayHi(condition){
    var msg;
    if(condition){
        msg = 'Hello world';
        console.log(msg); // Hello world
    }else{
        console.log(msg); // undefined
    }
}

sayHi(true);
sayHi(false);

3. 块级作用域声明

EcmaScript 6引入了块级作用域(block scope),块级作用域只能在块中被访问,以下两种情况可以创建块级作用域的变量。

  • 在函数中
  • 在被{}包裹的块中

3.1 let声明

let关键字的作用类似var,用来声明变量,不同的是其声明的变量具有块级作用域。

'use strict';
function sayHi(condition){
    if(condition){
        let msg = 'Hello world';
        console.log(msg); // Hello world
    }else{
        console.log(msg); // ReferenceError: msg is not defined
    }
}

sayHi(true);
sayHi(false);

如上面例子,msg只能在if语句块中被访问,在else中访问会产生ReferenceError错误

3.2 不能重复声明

使用var关键字声明的变量,在同一个作用域可以重复进行声明,后面的将会覆盖前面的。而使用let关键字声明的变量,在同一个作用域不能重复声明(不管之前是用varlet, 或者const声明),否则将会触发SyntaxError错误。

'use strict';
var a = 1;
var a = 2;

//var b = 1;
//let b = 2; // SyntaxError: Identifier 'b' has already been declared

//let c = 1; 
//let c = 2; // SyntaxError: Identifier 'c' has already been declared

const d = 1;
let d = 2; // SyntaxError: Identifier 'd' has already been declared

在变量包含的作用域是用let关键字声明的变量,不会生成错误。如下面代码,在if语句中,新的变量a将覆盖外面的变量a

'use strict';
let a = 1;

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

3.3 const声明

使用const关键字声明的变量将作为常量使用,一旦被赋值,将不能再被改变,因此const关键字声明的变量必须同时进行初始化,否则会抛出SyntaxError错误

'use strict';
const maxItems = 30;

const name; // SyntaxError: Unexpected token

上面例子中的name变量没有进行初始化,因此将触发SyntaxError

const关键字与let关键字相同的地方

  • 声明的变量具有块级作用域。
  • 在相同的作用域,重复声明变量将会抛出SyntaxError错误

const关键字与let关键字不同的地方

  • const声明的变量不能重复进行赋值, 否则将抛出TypeError错误

const声明的对象不能改变,但是对象中的属性可以进行改变。const绑定的是对象的引用,对象中实际的值是可以改变的。

'use strict';
const person = {
    name:'Mary'
};

person.name = 'Jim';
person = {
    name:'Bary'
}; // TypeError: Assignment to constant variable

3.4 临时死区

使用letconst声明的变量,只有在声明之后才能够使用,否则会触发ReferenceError错误,即使在ES5中可以安全使用的typeof关键字在ES6中也不能保证可以安全使用。这种特殊行为就叫做临时死区(Temporal Death Zone)。

'use strict';
if(true){
    console.log(typeof value); // ReferenceError: value is not defined

    const value = 'blue';
}

如上面代码,由于临时死区的存在,value变量在声明之前是不能被访问的。
Javascript引擎遇到一个代码块并且代码块中存在变量声明,那么要么进行变量声明提升(hoisting),将变量提升到函数或者全局作用域顶部进行声明(使用var关键字);要么将变量放入临时性死区(使用const或者let关键字),尝试访问临时性死区中的变量将会导致ReferenceError错误。在遇到变量声明(const或者let关键字)后,该变量将被移出临时性死区,变量就可以被访问了。

'use strict';
console.log(typeof value); // undefined
if(true){
    let value = 'blue';
}

如上面代码,value变量并未放入临时性死区,使用typeof关键字仍然是安全的。

4. 循环

4.1 循环的块级作用域

实际开发中比较长使用的是在循环中使用块级作用域,在Javascript中,var关键字在循环中使用有着很多不被其他语言开发者了解的缺陷。

for(var i=0;i<10;i++){
    // do something
}

console.log(i); // 10

例如上面代码中,开发者希望达到的效果是i在循环之后不能访问,但是由于Javascript会进行变量声明“提升”,i在循环结束之后,仍然能够访问。
使用let关键字,可以有效避免这个问题。

for(let i=0;i<10;i++){
    // do something
}

console.log(i); // ReferenceError

4.2 函数的循环

使用var关键字创建的变量在循环内部的函数中使用有一些问题。例如下面的代码,开发者期待输出从09,但是由于i在循环之后仍然能够访问,funcs中的每个函数引用的是同一个i,因此在调用后输出了十次10

var funcs = [];
for(var i = 0;i < 10;i++){
    funcs.push(function(){console.log(i);});
}

funcs.forEach(function(func){
    func(); // / outputs 10 ten times
});

上面的问题可以通过立即执行函数(Immidiately Invoked Function Expressions,IIFEs)来解决

var funcs = [];
for(var i = 0;i < 10;i++){
    funcs.push((function(val){
        return function(){console.log(val);};
    })(i));
}

funcs.forEach(function(func){
    func(); // outputs 0, then 1, then 2, up to 9
});

如上面的代码,使用立即执行函数强制将i作为参数传入到每个函数中,这样每个函数保存的是循环过程中i的副本,因此可以正确输出。

4.3 在循环中使用let

使用let关键字可以用比较简单清晰的代码解决上面的问题。

'use strict';
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
});

如上面代码,使用let会在每次循环过程中,创建一个新的变量i,因此每个函数读取到的是每次循环的i的副本,每个i的副本的值由循环初始化时i的值决定。
for infor of循环中,let关键字有同样的特性。如下面代码,在每次循环时创建一个新key绑定,这样每次循环都一个新的key变量,因此每个函数输出不同的key。如果使用var关键字代替let,那么所有函数将输出相同的值c

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

注意:let关键字在循环中的特性在规范中定义,但是跟let关键字的“非提升”特性并不相关。实际上,早起的一些let实现并没有实现上述在循环中的特性。这些特性是逐渐添加的。

4.4 在循环中使用const

EcmaScript 6规范并没有在循环中使用const给予禁止,但是在不同的循环类型(for, for in, for of),其行为是不同的。在for循环中,const可以在循环初始化时使用,但是如果尝试修改其值,那么将会抛出错误。如下面代码,i在初始化时时没问题的,但是在调用i++时,将会发生TypeError错误,因为++操作尝试修改一个常量的值。

'use strict';

var funcs = [];
for(const i=0;i<10;i++){ // TypeError: Assignment to constant variable
    funcs.push(function(){
        console.log(i);
    });
}

for in, for of循环中使用const关键字则不会发生错误

'use strict';
var funcs = [],
    object = {
        a: true,
        b: true,
        c: true
    };

// doesn't cause an error
for (const key in object) {
    funcs.push(function() {
        console.log(key);
    });
}

funcs.forEach(function(func) {
    func();     // outputs "a", then "b", then "c"
});

如上面代码,使用const关键字与let关键字的不同之处只在于,key变量在块中不能被修改。由于在for in, for of循环中,每次遍历都会绑定一个新的key而不是尝试修改原来的key,因此const关键字在for in, for of中使用是没问题的。

4.5 全局作用域

在全局作用域使用let或者const关键字与var是不同的。当在全局作用域使用var关键字时,创建一个新的全局变量,并将其绑定到全局对象的属性上。因此,可以使用var覆盖全局对象上已经存在的变量。

// in a browser
var RegExp = "Hello!";
console.log(window.RegExp);     // "Hello!"

var ncz = "Hi!";
console.log(window.ncz);        // "Hi!"

上面的代码中,window对象原有的RegExp被修改,ncz被定义为一个全局变量,并写入全局对象的属性上。
letconst关键字会创建在全局作用域创建一个变量,但是并不会写入到全局对象的属性。因此,letconst关键字创建的变量不会覆盖全局变量,而只能创建一个优先读取的“影子”变量。

// in a browser
let RegExp = "Hello!";
console.log(RegExp);                    // "Hello!"
console.log(window.RegExp === RegExp);  // false

const ncz = "Hi!";
console.log(ncz);                       // "Hi!"
console.log("ncz" in window);           // false

如上面代码,RegExpwindow.RegExp是不同的,ncz也并不在window对象上。因此,当开发者不需要在全局对象上创建变量,使用letconst关键字在全局作用域创建对象相比var是安全的。

5. 现有的最佳实践

由于let关键字的特性更符合其他语言的习惯,不会产生var关键字导致的各种问题,因此尽量使用let关键字广被开发者倡导。
由于大部分变量实际上不会改变,而修改变量会导致不可测的bug,因此对于那些不会发生改变的变量,要使用const关键字

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

推荐阅读更多精彩内容