javascript作用域
在一些语言中,声明和定义存在明确的区分。声明一个变量仅指给它指定一个标志符来宣称它的存在。而定义则代表声明后还要进行赋值。
在 javascript 中,声明和定义是可以互换的概念。因为所有变量都会在声明之后给定一个值。如果你没有显式赋值,会自动给定一个隐含值 undefined。
静态作用域
javascript 使用静态作用域。静态作用域又称词法作用域,静态作用域下函数作用域在定义(声明)时已经确定,如未找到自身作用域内的变量,则沿作用域链向上,在上级作用域中查找,直到全局。
function foo() {
console.log(a);
}
function bar() {
let a = 3;
foo();
}
let a = 2;
bar(); // 输出2 详见下面的说明
静态作用域:
foo 的作用域在声明时就已经确定,当 foo 被调用时,在 foo 自身的作用域中找不到变量 a,此时以定义 foo 时的作用域链为准,向作用域链上级查找。foo 定义时的作用域链为:foo 作用域——全局作用域。而在全局作用域中存在 a = 2,故输出结果为 2。
动态作用域:
如果是动态作用域,foo 的作用域在 foo 被调用时确定,作用域链向上是 foo 被调用时的环境。在 foo 的作用域中找不到 a,则沿作用域链向上,在 foo 被调用的环境即 bar 的作用域中查找,bar 中 a = 3,故在动态作用域规则下应输出 3,而 js 使用静态作用域,只能输出 2。
总结:在函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,静态作用域下是去函数定义时的环境中查询,动态作用域下是去函数调用时的环境中查询。
在函数中定义的参数和声明的局部变量,其作用域范围仅在函数内部,当函数调用完毕,参数与局部变量都会被销毁。
作用域、上下文、执行环境
作用域(Scope):编译过程中,负责收集并维护所有声明的标志符,确定当前执行代码对这些标志符的访问权限的一套规则。简而言之作用域就是变量的可见性。
上下文(Context):函数调用时的 this 值。this 来自执行环境对象。
P.S. new 关键字调用函数时,上下文 this 被设置为被调用的函数实例,即新构造的对象。
严格模式调用函数,上下文 this 为 undefined。(详见js关键字this)
执行环境(Execution Context):执行环境又称执行上下文,当代码流转到可执行代码时,会进入一个执行环境,执行环境有创建和执行两个阶段。创建时会创建一个执行环境对象,其中包含作用域链,变量对象与上下文。
当前所有执行环境构成一个栈,栈底部始终是全局环境,顶部是当前活动的执行环境,随执行流的变化,执行环境入栈或出栈。全局环境的执行环境对象就是 Window 对象。
块作用域
ES6 前只有函数作用域和全局作用域,ES6 中添加了块作用域。块是由一对花括号括起来的一系列语句。块作用域指的就是那些仅仅在代码块内有效的变量。块通常是控制流语句的一部分,如 if 或 for,定义独立的块也是合法的。ES6 中 let 与 const 关键字声明的变量处在块作用域中,var 声明的变量在代码块外也可以访问到。
const bar = 1;
console.log(foo); // undefined 块内变量提升,相当于只声明,未赋值。
console.log(bar); // 1
{
var foo = 2; // 相当于已经声明只进行赋值
const bar = 3; // 块内声明只在块内生效,这里块内声明的变量屏蔽了块外变量,块内无法访问块外变量
console.log(foo); // 2 undefined 赋值为2
console.log(bar); // 3
}
console.log(foo); // 2 块外也能访问var声明的变量
console.log(bar); // 1 块外访问不到块内变量bar,只能访问到块外的变量bar
变量屏蔽
使用 let 或 const 在块作用域外与块作用域内声明相同名字的变量,块内声明的变量会屏蔽块外的,使块外的变量不可访问。这个特性可以防止变量污染。函数作用域同理。
const foo = {
objName: "foo",
};
const bar = foo;
const x = 1;
{
let foo = 5;
console.log(foo); // 5 外部foo变量屏蔽,访问不到
console.log(bar.objName); // foo
bar.objName = "bar"; // 对象为引用型值,本身并不构成作用域,虽然在块中修改,但是在全局都生效
let x = 10;
console.log(x); // 10 外部x变量屏蔽,访问不到
}
console.log(foo); // bar 对象是引用型,变量bar与变量foo引用自相同对象,在bar中更改对象后,foo引用到的也是被修改后的对象
console.log(bar); // bar
console.log(x); // 1 块中变量屏蔽,块外变量不受影响
对象是引用型,更底层的解释是对象存储在一个内存位置中,将对象通过变量赋值给其它变量,这些变量都引用自相同内存位置的同一对象,因此在一个变量上操作改变对象,内存中的对象被更改,其他变量引用的也是改变后的对象。
如果将对象直接赋值给不同变量,则每个变量中的对象看起来是相同的,但其实是存储在不同内存位置的不同对象。
const 声明的基本类型值无法被更改,但 const 声明的数组和对象可以被更改,因为对象(数组)是引用型,const 固定了指向对象(数组)内存位置的引用,但对象(数组)在内存中的内容没有固定。
const foo = { num: 1 };
const foo1 = foo;
const bar = { num: 1 };
console.log(foo === foo1); // true 通过变量赋值,foo与foo1实际引用自相同对象
console.log(foo === bar); // false 直接赋值,foo与bar是存储在不同内存位置的不同对象,但对象内容相同
foo.num = 2;
console.log(foo === foo1); // true foo修改了内存中的对象,foo1引用自那个对象,所以foo1也被自动更改了
console.log(foo, foo1, bar); // {num: 2}, {num: 2}, {num: 1} foo与foo1引用自相同对象,bar和前两者引用自不同对象
变量提升
在 ES6 引入 let 和 const 之前,变量都是用 var 来声明的。使用 var 声明的变量在当前作用域中任何地方都可以使用,甚至可以在声明前使用。这是因为使用 var 声明的变量采用了提升机制,javascript 会扫描整个作用域(函数或全局作用域),任何使用 var 声明的变量都会被提升至当前作用域的顶部,被提升的只是对变量的声明,而不是赋值。
console.log(foo); // undefined 变量提升,只提升声明,默认赋值为undefined
var foo = 1; // 赋值为1
console.log(foo); // 1
// 等价于
var foo;
console.log(foo);
foo = 1;
console.log(foo);
let 与 const 没有变量提升机制,在声明前无法使用
console.log(bar); // referenceError 直接报错,无法运行
let bar = 0;
console.log(bar);
由于 var 没有块作用域,且 var 有变量提升,故在当前作用域中,尽管可以在块内再次使用 var,但并不会发生变量屏蔽,不能创建同名的新变量。
var foo = 0;
if (foo === 0) {
var foo = 1;
console.log(foo); // 1
}
console.log(foo); // 1
// 等价于
var foo = 0;
if (foo === 0) {
foo = 1;
console.log(foo); // 1
}
console.log(foo); // 1
临时死区 TDZ
由于 ES6 中 let、const 有块作用域,且 let、const 没有变量提升机制,故在块中再次声明同名变量会不小心产生临时死区。要防止临时死区应注意将所有声明都放在当前作用域顶部。
let foo = 0;
if (foo === 0) {
console.log(foo); // referenceError 这里就是临时死区
let foo = 1; // 因为块中用了let声明,块内的foo屏蔽了块外的foo,而上一句却在块中未声明foo时就使用foo
console.log(foo);
}
console.log(foo);
ES5 中常用的类型检测方法,在 ES6 中就会因为临时死区而报错
if (typeof foo === "undefined") {
// referenceError
console.log("foo is safe to use");
}
let foo = 1;
总结 var、let、const
var:
- 有变量提升机制。
- 没有块作用域,只有函数作用域和全局作用域,块中不会产生变量屏蔽,在函数作用域中才有变量屏蔽。
- 虽然在同一作用域同一变量名可以多重声明,但不创建新变量,仅相当于赋值操作。
- 在全局环境声明的变量会自动挂载为 window 的属性,不仅可以通过变量名访问,还可以通过 Window 属性访问,Window.变量名。
let、const 相同点:
- 没有变量提升机制。
- 有块作用域,块内会产生变量屏蔽,警惕因变量屏蔽、无提升机制造成的临时死区。
- 同一作用域同一变量名只能声明一次。
- 在全局环境声明的变量不会挂载为 Window 的属性,只能通过变量名访问。
let、const 区别:
- let 用于声明变量、const 用于声明常量。
- const 声明与赋值必须同时进行,基本型值定义后无法更改,如字符串、数字等。
- 数组、对象为引用型值,const 声明后固定了引用的内存位置,但未固定内容,故数组与对象可更改。
函数提升
函数的声明也会被提升至当前作用域的顶部,允许函数在声明前使用。赋值给变量的函数表达式不会提升,作用域规则与变量相同。函数提升更高于变量提升,当函数声明与变量声明同名时,函数声明在前,变量声明在后。(尽量避免函数与变量同名)
f(); // f 函数提升,可以提前调用
g(); // referenceError:g未定义 函数未提升,变量未提升,无法调用,直接停止报错
h(); // typeError:h不是函数 函数未提升,变量提升只是声明提升,并未赋值,h为undefined
function f() {
console.log("f");
}
let g = () => console.log("g");
var h = () => console.log("h");
严格模式
ES5 允许隐式全局变量,如果忘记使用 var 声明变量,Javascript 会认为你在引用全局变量,如果全局变量不存在,会自动创建一个。
function f() {
a = 1;
console.log(a); // 1
}
f();
console.log(a); // 1
为了防止这种情况以及 this 默认绑定到全局等情况,引入了严格模式。在全局或函数的开头写 “use strict”,单双引号都可以,就开启了严格模式。
由于在全局中开启严格模式会应用到所有脚本代码中,故使用时要谨慎。很多网站在部署前会整合脚本代码,若一个脚本代码用了全局严格模式,则所有代码在整合后都会应用严格模式,为了防止这种情况,应该将用了严格模式的脚本代码封装在一个 IIFE(立即执行函数)中,这样就不会干扰整合后的其它脚本。
(function () {
"use strict";
// 代码区
})();