全面解析 js 中的 let/var/const

对于从 C 系语言转来学习 js 的开发者而言,最难理解的就是 js 中的varletconst 的设计。在其他语言中,每个变量默认都有自己的作用域,然而在 js 中并不是这样。直到ECMAScript 6标准的确立,才给 js 开发带来了更简单的作用域管理。ECMAScript 6是在 2015 年正式发行,所以又叫ECMAScript 2015。本篇将全面的分析varletconst关键字的使用及作用域。

最佳实践


想急于知道最佳实践,又不想看完冗长的文章,那么只需要记住,最佳实践:

  • 如果可以,不要使用var来定义变量,而是使用let
  • 优先使用const,而非 let。

undefinedis not defined的区别


开始前,有必要先了解这两个重要概念,*** is not defined是一个运行时报错:

console.log(a);
// 运行报错: "Uncaught ReferenceError: a is not defined"

原因很简单,上下文并没有定义 a 变量。

undefined是全局对象的一个属性,在浏览器环境里,undefined就是window的一个属性,在运行时创建:

console.log(window.undefined);
// 输出 undefined

可以认为undefined就是全局作用域下的一个变量,且该值不可写,是只读的。undefined的是 js 的一个原始类型,该类型是undefined类型,该类型只有一个真值,就是undefined

如果觉得这段话很绕,那么可以做个类比,假设window对象有个属性叫 bl。bl 的类型是BooleanBoolean是 js 的一个原始类型。它有两个真值: true | false

undefined的字面意思是未定义,但在代码中的真正含义并不像它的字面意思那样,它的真实意思是:定义了,但是没有初始化的变量,即uninitialized:

var a;
console.log(a);
// 输出 undefined

所以,在 js 中大部分情况下,undefined = declared but not initialized或者undefined = uninitialized。换句话说,定义了但没有被赋值的变量被称为undefined

但是typeof这个被称为安全运算符的是个例外:

console.log(typeof(a)); // undefined
console.log(a);         // "Uncaught ReferenceError: a is not defined"

上边的代码,第一行正常输出undefined,第二行报错。可以看到,即使没有定义变量 a,typeof依然输出了undefined,而不是报运行时错误。但typeof并不总是安全的,这个后文在时间死区部分在讲。

var 与 变量提升 (Hoisting)


在很多语言中,默认一对大括号就是一个作用域,作用域内的变量,无法在作用域外访问,以 C 语言为例:

{
	int x = 10;
}

printf("%d", x); // 编译报错,无法访问 x。

然而,在 js 中,当你用 var 定义一个变量的时候,上述规则就会被打破,比如下边的 js 代码:

const bl = false;

if (bl) {
	var x = 10;
} else {
	console.log("The value of x in else block is: ", x);
}

console.log("The value of x in outer is: ", x);

上述代码,不会报错,可以正常执行,并输出:

The value of x in else block is:  undefined
The value of x in outer is:  undefined

这个特征在 js 里称为hoisting,这里就翻译为变量提升吧。事实上,在解释执行时,上述代码会被调整为:

const bl = false;
var x; // hoisting
if (bl) {
	x = 10;
} else {
	console.log("The value of x in else block is: ", x);
}

console.log("The value of x in outer is: ", x);

变量x的声明被提升到了全局作用域上,而变量初始化依然保留在原味。如果在函数内部,则变量声明会被提升到函数的最开始,例如:

function printX() {
  const bl = false;
  if (bl) {
  	var x = 10;
  } else {
  	console.log("The value of x in else block is: ", x);
  }
  console.log("The value of x in outer is: ", x);
}

// call printX function
printX();

上述代码依然可以正常执行,输出:

The value of x in else block is:  undefined
The value of x in outer is:  undefined

只不过,这次变量的声明提升,只会发生在函数内部,不会超出函数的范围:

function printX() {
  const bl = false;
  var x; // hoisting
  if (bl) {
  	x = 10;
  } else {
  	console.log("The value of x in else block is: ", x);
  }
  console.log("The value of x in outer is: ", x);
}

// call printX function
printX();

由于这个hoisting特性,很不符合直觉,也容易引发一些难以察觉的问题。所以 ES 6 就带来了一个block-level scope,块级作用域这个概念。

Block-Level Scope


块级作用域这个功能的出现,终于让 js 能想其他语言一样,有了正常的变量作用域,用let定义的变量,或者用const定义的常量,都满足块级作用域。

const bl = false;

if (bl) {
	let x = 10; // 用 let 来限制块级作用域
} else {
	console.log("The value of x in else block is: ", x);
}

console.log("The value of x in outer is: ", x);

上述代码执行,会报错:

ReferenceError: x is not defined

let定义的变量,就不会发生hoisting提升。变量 x 只在 if 的条件块能被定义和初始化,出了该作用域,变量就无法访问。

const 常量


const关键字用来定义常量,常量一旦定义,就不可更改。所以就要求,常量必须在声明的时候,就初始化。

const x = 10; // 正确

const y;
y = 20; // 错误

在用const定义对象的时候,需要注意,对象本身不可更改,但是对象内部的值可以被修改:

const person = {
	name: "Jack"
};

person.name = "Rose"; // 正确

person = {}; // 错误

时间死区


文章开头提到过,js 中typeof是一个’安全操作符’:

console.log(typeof x); // 输出: undefined
if(true) {
	let x = 10;
}

变量 x 用let定义,所以 x 只能在 if 块中有效,但是typeof依然不会报错,而是输出undefined。但是下边代码就出现了例外,typeof这个安全操作符也不安全了:

if(true) {
	console.log(typeof x); // 报错: ReferenceError!
	let x = 10;
}

// 执行报错: "Uncaught ReferenceError: Cannot access 'x' before initialization"

这在 js 社区里被称为时间死区 TDZ(Temporal Dead Zone)let定义的变量 x,只在 if 块中生效,且该区域严格执行未定义的变量不能访问这一原则,就连typeof也不例外。需要注意,const同样适用这一原则:

if(true) {
	console.log(typeof x); // 报错: ReferenceError!
	const x = 10;
}

// 执行报错: "Uncaught ReferenceError: Cannot access 'x' before initialization"

在 for 循环中的块级绑定


在 for 循环中,如果在用var定义计数变量,通常会发生一些难以理解的输出:


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

funcs.forEach(function(f) {
	f(); // 连续输出 10 次 10。
});

对大部分开发者,都希望这部分代码输出 0…9,但由于var变量的hoisting特性,导致每个函数捕获到的都是同一个变量 i。所以最终都是 10。所以以前的 js 开发者们通过即执行函数来捕获一个 i 的 copy 值解决:

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

funcs.forEach(function(f) {
	f(); // 输出 0...9
});

每次循环,都执行一个函数,i 通过函数参数的形式传入函数内部,就会发生值拷贝,内部的函数就会捕获到一个拷贝后的值。这给很多从其他语言转来 js 的开发者带来很大的疑惑。

最终,ES 6 的块级作用域,运行我们用let轻松解决上述问题:

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 的值为上次的变量 + 1,这样函数每次都捕获到自己的变量 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"
});

let在 for 循环中的特性是特别定义的行为,与非 hoisting 没有必然联系。

在 for 循环中使用 const


const 在不同的循环中,有着不同的表现。在 for-i 循环中,使用 const 会抛出错误:

for (const i = 0; i < 10; i ++) {
	console.log(i);
}

// TypeError: Assignment to constant variable.

错误具体是在, i++ 上报出的,试图修改一个常量而报错。但是,在for-infor-of中,const的表现就和let一样了。

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

在 for-in 和 for-of 循环中,每次循环都会创建一个新的 const 常量与块绑定,这一点和let是一样的。

全局块绑定


letconstvar的另一个区别,表现在全局作用域上。当在全局作用域上用var创建一个变量时,这个变量会被挂载在全局对象上,如果是浏览器,这个全局对象就是 window。

var r = "hello";
console.log(window.r); // hello

如果用let或者const定义全局变量,则该变量不会被挂载在 window 上:

let r = "hello";
console.log("r" in window); // false

(全文完)

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇