作用域是指变量所可作用的范围,分别有全局作用域和局部作用域。
要了解作用域就要先了解变量提升。
变量提升是指在JS中执行上下文发作方式的一种。具体表现方式就是所有通过var声明的变量会提升到当前作用域的最前面。
function foo() {
console.log(temp);
}
function bar() {
console.log(temp);
var temp;
}
foo(); // ReferenceError: temp is not defined
bar(); // undefined
可以看到用var声明了的并不会报错。因为其实函数bar等同于
function bar() {
var temp;
console.log(temp);
}
大多数类C语言语法的语言都拥有块级作用域。在一个代码块中定义的所有变量在代码块的外部是不可见的。定义在代码块中的变量在代码块被执行结束后会被释放掉。
尽管JavaScript的代码似乎支持块级作用域,但实际由于变量提升,并不能很好的支持。
所以在ES6中规定了let和const来支持块级作用域。但是,依旧存在暂时性死区。
let
let基本使用方法和var相同,而且声明的变量只在其块和子块中可用,这点也与var相同。二者之间最主要的区别在于var声明的变量的作用域是整个封闭函数。
function foo() {
if(true) {
var temp = 5;
console.log(temp);
}
console.log(temp);
}
function bar() {
if(true) {
let temp = 5;
console.log(temp);
}
console.log(temp);
}
foo(); // 5 和 5
bar(); // 5 和 "ReferenceError: temp is not defined
let声明的变量的作用域只是外层块,而不是整个外层函数。
我们可以利用这个特性来代替立即执行函数。
// IIFE
(function(){
var temp = xxx;
/*
other code
*/
}())
// 块级
{
let temp = xxx;
/*
other code
*/
}
const
const的用法跟let差不多,但是const一定要初始化,不初始化是会报错的。
const temp = 4;
// 没有初始化报错
const t; // SyntaxError: Missing initializer in const declaration
const是块级作用域,const跟let的语义相似,就是用来声明常量的,但是一但声明了就不能更改。值得注意的是const声明的变量记录的是指针,不可更改的指针,如果const所声明的是对象,对象的内容是可以更改的。
// 重新赋值声明导致报错
const PI = 3.14;
PI = 3.1415926; // TypeError: Assignment to constant variable.
// 给对象增加属性不会导致 obj 的指针变化,所以不会报错
const obj = { foo: 2 };
obj.bar = 3;
console.log(obj); // {foo: 2, bar: 3}
暂时性死区
使用let和const声明的变量,在声明没到达之前,访问该变量都会导致报错,就连一直以为安全的typeof也不再安全。
// TDZ1
function foo() {
// TDZ 开始
console.log(typeof temp);
let temp = 5; // TDZ 结束
}
foo(); // ReferenceError: temp is not defined
报错的是referenceError,如果使用var声明的话,temp输出应该是undefined,从let声明的变量的块的第一行,到声明变量之间的这个区域被称作暂时性死区。凡是在这个区域使用这些变量都会报错。
// TDZ2
function bar() {
console.log(typeof temp);
}
bar(); // undefined
在函数里没有用let声明temp的时候,temp是undefined,讲道理在let声明前也应该是undefined,然而foo函数却报错了,证明了就算是在未达到let声明的地方,但是在用let之前已经起到了作用。这是不是说明其实let也有提升(这个提升并不是var的那种提升,只是有影响),只是在TDZ使用的时候报错了,而不是undefined。
事实上,当JS引擎检视下面的代码块有变量声明时,对于var声明的变量,会将声明提升到函数或全局作用域的顶部,而对let或const的时候会将声明放在暂时性死区内,任何在暂时性死区内访问变量的企图都会导致“运行时”错误(runtime error)。只有执行到变量的声明语句时,该变量才会从暂时性死区内被移除并可以安全使用。
禁止重复声明
在同一个块内,let和const不能声明相同的标识符。禁止情况包括:
let或const或let和const
var和let或const
函数参数与let或const
// let 和 let
let foo = 1;
let foo = 2;
// let 和 const
let foo = 1;
const foo = 2;
// var 与 let
var foo = 1;
let foo = 2;
// 函数参数与 let
function bar(foo) {
let foo = 1;
}
以上情况都是会报syntaxerror。但是在嵌套的作用域内使用let声明同一变量是被允许的。
var foo = 1;
{
// 不会报错
let foo = 2;
// other code
}
同时因为是let和const是块级作用域,声明的变量在当前块使用完之后就会被释放,所以就算使用相同的标识符也不会覆盖外部作用域的变量,而var是会覆盖外部作用域的变量的。
function foo() {
var bar = 1;
{
let bar = 2;
}
console.log(bar);
}
function zoo() {
var bar = 1;
{
var bar = 2;
}
console.log(bar);
}
foo(); // 1
zoo(); // 2
在 ES6 的发展阶段,被广泛认可的变量声明方式是:默认情况下应当使用 let 而不是 var 。对于多数 JS 开发者来说, let 的行为方式正是 var 本应有的方式,因此直接用 let 替代 var 更符合逻辑。在这种情况下,你应当对需要受到保护的变量使用 const 。
在默认情况下使用 const ,而只在你知道变量值需要被更改的情况下才使用 let 。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错误。
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加let和const命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。
顶层对象的属性
顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。
window.a = 1;
a // 1
a = 2;
window.a // 2
上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。
顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。
ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。
var a = 1;
// 如果在 Node 的 REPL 环境,可以写成 global.a
// 或者采用通用方法,写成 this.a
window.a // 1
let b = 1;
window.b // undefined
上面代码中,全局变量a由var命令声明,所以它是顶层对象的属性;全局变量b由let命令声明,所以它不是顶层对象的属性,返回undefined。
globalThis 对象
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
- 浏览器里面,顶层对象是
window,但 Node 和 Web Worker 没有window。 - 浏览器和 Web Worker 里面,
self也指向顶层对象,但是 Node 没有self。 - Node 里面,顶层对象是
global,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this关键字,但是有局限性。
- 全局环境中,
this会返回顶层对象。但是,Node.js 模块中this返回的是当前模块,ES6 模块中this返回的是undefined。 - 函数里面的
this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。 - 不管是严格模式,还是普通模式,
new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用。
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
// 方法一
(typeof window !== 'undefined'
? window
: (typeof process === 'object' &&
typeof require === 'function' &&
typeof global === 'object')
? global
: this);
// 方法二
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
ES2020 在语言标准的层面,引入globalThis作为顶层对象。也就是说,任何环境下,globalThis都是存在的,都可以从它拿到顶层对象,指向全局环境下的this。
垫片库global-this模拟了这个提案,可以在所有环境拿到globalThis。