ES5 中的作用域
function
作用域(由function
声明的作用域,包括函数表达式、函数声明或是用Function
所创建对象)
// 函数表达式
function fun1 (){
var test = "test";
return test;
}
// 函数声明
var fun2 = function () {
var test = "test";
return test;
}
console.log(test);
// ReferenceError: test is not defined
console.log(fun1());
// "test"
// test 在 fun 这个函数的作用域内才能被访问
- 对象级别的作用域(使用
JavaScript
对象生成作用域)
var obj = {
test: "test",
method() {
return this.test;
}
}
console.log(test);
// ReferenceError: test is not defined
console.log(obj.method());
// "test"
// test 在 obj 这个对象的作用域内才能被访问
总的来说,ES5
中作用域依附于对象或函数存在,如果需要创建一个单独作用域,那么必须创建一个对象或函数。
但是会有很多人疑问:在 ES5
中像 if\for\while\do...while\switch
等也会有 {}
这不能创建一个作用域吗?在严格模式下这里面的声明变量是不允许的(通俗的讲就是错的),而目前绝大多数的浏览器都是可以用的原因,只是浏览器放宽了要求罢了。
大概描述了下 ES5
中的作用域,那就该介绍了主角了:块级作用域。
块级作用域
在具体的描述之前,想象一个场景:有一个数组,遍历这个数组,取得数组的值,然后根据该值有一个异步的调用。代码如下:
var arr = ["a", "b", "c"];
for(var i = 0, len = arr.length; i<len; i++){
setTimeout(function() {
console.log(arr[i]);
}, 500);
}
看到这里,一定会有人说代码写错了,当然如果你觉得没错,可以打开浏览器试试,看看输出什么结果。由于异步调用并不会立即执行,并且 {}
并不能保证一个独立的作用域,因此当异步执行时,i
的值为 3
而 arr[3]
就为 undefined
。正确的代码如下:
var arr = ["a", "b", "c"];
for(var i = 0, len = arr.length; i<len; i++) {
// 用一个立即执行的函数保存 i 的值
(function(index) {
setTimeout(function() {
console.log(arr[index]);
},500);
})(i)
}
// console: "a" "b" "c"
// 或者
var arr = ["a", "b", "c"];
for(var i = 0, len = arr.length; i<len; i++) {
// 使用 setTimeout 保存 i 的值
setTimeout(function(index) {
console.log(arr[index]);
}, 500, i);
}
我们不得不实现一个独立的作用域来锁住 i
的值。因此我们不能在异步函数中直接使用 for
循环中的 i
。
那么在了解了 ES5
中作用的局限后,来看看 ES6
中,如何实现上述的效果:
var arr = ["a", "b", "c"];
let len = arr.length;
for(let i = 0; i<len; i++){
setTimeout(function() {
console.log(arr[i]);
}, 500);
}
// console: "a" "b" "c"
出人意料的简单!一样的代码,仅有的区别在于:ES5
的代码中变量是用 var
声明,而在 ES6
中,使用 let
声明。就像是 let
关键字 锁 住了 i
的值,产生了 {}
级别的作用域。如下所示:
{
let letTest = "test";
var varTest = "test";
}
letTest // ReferenceError: letTest is not defined.
varTest // 2
ES6
新增了 let
命令,用于变量声明。用法类似于 var
,但是 let
声明的变量只在 let
所在的 {}
内有效。
文章刚开始的例子就能说明这个问题,由于 let
声明的变量只在每一个 for
循环的 {}
中有效,而 var
声明的变量由于 声明在 for
循环外,循环内是用的是同一个 var
变量,导致意外的结果。
let
let
变量的一些特点。
不存在变量提升
let
不会像 var
发生变量提升,所以变量的使用一定要在声明之后。
console.log(foo); // 由于变量提升,输出undefined
console.log(bar); // 不存在变量提升,报错 ReferenceError
var foo = "test";
let bar = "test";
暂时性死区(temporal dead zone,简称TDZ)
只要块级作用域内存在 let
命令,即使外部有该变量的声明,也不会有效。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError 暂时性死区导致使用不了外部的 tmp 变量
let tmp;
}
ES6
明确规定,如果区块中存在 let
或 const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
typeof
常被我们用来判断变量的类型,由于 var
声明会导致变量提升,因此在 let
出现之前, typeof
可以认为是一个不会报错的行为(最多被判定为 undefined
)。但如果在 let
变量声明前使用 typeof
判断变量,就会报错(暂时性死区导致引用报错)。
但是有一点比较独特:假设一个变量根本就没声明呢?
typeof undeclared_variable // "undefined"
反而是 undefined
了,感觉变量默认都是 var
声明的样子??这点之后说明。
一个比较隐蔽的 TDZ:
// 由于默认值导致的 TDZ
function bar(x = y, y = 2) {
return [x, y]
}
bar() // 报错
// 等价于
{
let x = y // ReferenceError
let y = 2
}
// 由于 y 声明前的区域为 TDZ,不能引用 y,因此报错。
// 这样是 OK 的
function bar(x = 2, y = x) {
return [x, y]
}
bar()
// [2, 2]
注: 参数括号中的写法是 ES6
中给函数参数设置默认值的方式,=
之后的就为默认值。ES6
中参数的声明方式为 let
,会导致 TDZ
的出现。
总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
虽然 TDZ
会导致各种引用错误,但 ES6
对于 TDZ
的规定,主要是为了减少运行时错误,导致意外的行为。
不允许重复声明
// 报错:重复声明
function repeat() {
let test = "test1";
var test = "test2";
}
// 报错:重复声明
function repeat() {
let test = "test1";
let test = "test2";
}
// 报错:变量名与参数名一致
function func(arg) {
let arg
}
function func(arg) {
{
// 不报错 一个新的作用域
let arg
}
}
块级作用域的作用
在 ES5
中只有全局作用和函数/对象作用域,会带来很多问题。如下:
内层变量覆盖外层变量
var tmp = new Date();
function func() {
console.log(tmp);
if (false) {
var tmp = "hello world";
}
}
func() // undefined
由于变量的提升,覆盖了外层变量,即使 var tmp = "hello world"
永远不会被执行到。
用来循环计数的变量泄露
var str = 'hello';
for (var i = 0; i < str.length; i++) {
console.log(str[i]);
}
console.log(i); // 5
i
的功能其实只是计数而已,在 for
循环外应该不可见,但是它泄露了,而使用 let
就可以避免这个问题。
块级作用域与函数声明
讨论完了 let
和 var
,还有一个在 JavaScript
中很常见的东西: function
!!!这就比较复杂了。
首先函数有两种形式:
函数声明语句
function func() {
return "demo"
}
函数表达式
let func = function func() {
return "demo"
}
第二种形式在这里不过多的讨论,因为是赋值形式,所以它遵循的 let
和 var
的区别,在这里也推荐尽量使用函数表达式的形式来书写函数。
下面就来好好讨论下函数声明语句,ES5
规定,函数只能在顶层作用域或函数作用域之中声明,不能在块级作用域声明。以下的写法都是非法的。虽然浏览器不会报错,但在严格模式下依然会报错。
// 情况一
if (true) {
function func() {}
}
// 情况二
try {
function func() {}
} catch(e) {
...
}
但 ES6
引入了块级作用域后,明确允许了在块级作用域之中声明函数。并且 ES6
规定:在块级作用域之中,函数声明语句的行为类似于 let
,在块级作用域之外不可引用。
以下代码显示了 ES5
与 ES6
的区别:
function func() {
console.log('I am outside!');
}
(function () {
if (false) {
// 重复声明一次函数 func
function func() {
console.log('I am inside!')
}
}
f()
}());
// ES5 中就和以下代码一样
function f() {
console.log('I am outside!')
}
(function () {
function f() {
console.log('I am inside!')
}
if (false) {
}
f();
}());
// console: I am inside!
// 而在 ES6 中则是这样
function f() {
console.log('I am outside!')
}
(function () {
f();
}());
// console: I am outside!
很显然,这种行为差异会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6
在附录B里面规定:浏览器的实现可以不遵守上面的规定,有自己的行为方式。
主要允许的行为为:
- 允许在块级作用域内声明函数。
- 函数声明类似于
var
,即会提升到全局作用域或函数作用域的头部。 - 同时,函数声明还会提升到所在的块级作用域的头部。
注意,上面三条规则只对 ES6
的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作 let
处理。
const命令
const
声明一个只读的变量。一旦声明,变量的值就不能改变。和 let
命令类似,唯一不同的是 const
命令不能更改变量的值。同时,这也意味着, const
变量一旦声明,就必须立即初始化,不能留到以后赋值。
const foo
// SyntaxError: Missing initializer in const declaration
const
声明的常量,也与 let
一样不可重复声明。
var message = "Hello!"
let age = 25
// 以下两行都会报错
const message = "Goodbye!"
const age = 30
对于复合类型的变量,变量存的不是数据,而是指向数据所在的地址。所以只要保证该地址的数据不变,是可以更改对象内的内容的。这就好比你有一瓶雪碧,这瓶雪碧里有多少的量,它都只属于你。
const arr = [];
arr.push('Hello'); // 可执行
arr.length = 0; // 可执行
arr = ['Dave']; // 报错,内存地址发生变化
全局对象的属性
全局对象是最顶层的对象,在浏览器环境指的是 window
对象,在 Node.js
指的是 global
对象。 ES5
之中,全局对象的属性与全局变量是等价的,用 var
声明的变量,未声明直接使用的变量,都是全局对象下的一个属性。而在 ES6
中,为了保持兼容性, var
指令和 function
指令声明的变量,依旧是全局对象的属性,而 let
指令、 const
指令、 class
指令声明的变量,不属于全局对象的属性。
var test1 = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.test1; // 1
let test2 = 2;
window.test2; // undefined
这貌似能解决上面遗留的问题:
typeof undeclared_variable // "undefined"
这种形式下为什么是 undefined
了,全局对象下的一个属性,没有当然是 undefined
了。