变量作用域 scope 和作用域链 scope chain
var 变量的作用域和变量提升
JavaScript 有两种作用域:全局和局部。
在函数定义之外声明的变量是全局变量,它的值可在整个程序中访问和修改。
在函数定义内声明的变量是局部变量。每当执行函数时,都会创建和销毁该变量,且无法通过函数之外的任何代码访问该变量。
JavaScript 中,在函数体内 var 声明的变量是函数级作用域,是局部变量,在本函数体内可以访问,而且是在函数体内任意位置可以访问。
// JS 代码 function test() { console.log(val); var val = 'this is val'; console.log(val); func(); function func() { for (var i = 0; i < 5; i++) { } console.log('i: ', i); console.log('this is func'); } } test();
上述代码结果是:
undefined this is val i: 5this is func
JavaScript 解析器预解析代码的时候, test
函数作如下解析:
function test() { // 变量提升, 缺省值是 undefined var val; // 函数声明提升 function func() { // 变量提升 var i; for (i = 0, i < 5, i++) { } console.log('i: ', i); console.log('this is func'); } console.log(val); // 变量赋值 val = 'this is val'; console.log(val); func(); }
所以第一次 console.log(val)
时候并不会抛异常, 因为此时变量 val
是被声明过的,值是 undefined
。
理解 变量提升 ,写代码时应注意 变量污染 的坑。
如果声明变量时不加 var 直接
val = 1;
, 那么 val 是全局变量。
作用域链
作用域链包含了执行环境有权访问的所有变量和访问顺序。
作为单线程语言的 JavaScript,初始化代码时会创建一个全局上下文,每一次函数调用都会创建一个执行上下文,执行上下文及包含关系:
- 变量对象
- 变量
- 函数声明
- 参数(arguments)
- 作用域链
- 有权访问的变量和访问顺序(本作用域变量和所有父作用域变量)。即函数内部属性
scope
: 本函数有权访问的[变量、对象、函数]的集合
- this 值
如下代码:
function func_1() { var val_1 = 1; // 抛异常: ReferenceError: val_2 is not defined console.log(val_1, val_2); function func_2() { var val_2 = 2; // 输出:1 2 console.log(val_1, val_2); } func_2(); } func_1();
简言之, func_1 不能访问 func_2 中声明的变量, func_2 可以访问 func_1 中声明的变量。
当在作用域内访问一个变量 x
时,JavaScript 的查找顺序是这样的:
- 当前作用域
var x
的定义 => 2.x
形参 => 3. 函数自身名称是否是x
=> 4. 上级作用域从 1 开始查找
ES6 中的 let 和 const
ES6 的 let 和 const 实现了块级所用域的变量声明方式,使用 let 和 const 声明变量能有效避免由于变量提升导致的变量污染的问题。
用 let 和 const 声明的变量作用域是代码块,这个设计比较符合大多数人的思维方式。(代码块简单来说就是 {}
大括号包着的区域)
function test() { if (true) { var a = 'a'; let b = 'b'; } // 输出: a console.log(a); // 抛异常:ReferenceError: b is not defined console.log(b); } test();
关于 const 的作用有必要正确理解:
严格来说, const 声明了一个指向变量的指针,并不是说 const 声明的变量不可改变, 而是该指针指向的地址不可改变。
MDN 的例子很赞,这里直接拷过来看:
简言之: this 总是指向调用该函数的对象。
全局上下文 global context
// 在浏览器中console.log(this === window); // true
函数上下文 function context
在函数中访问 this 时, this 指向调用该函数的对象。
1)全局对象
// 全局变量val = 1;function test() { console.log(this.val); } test(); // 1
上例中,调用 test 函数的对象并不是一个自己声明的函数或对象,此时 this 默认值为全局对象。
2) 调用对象
var testObj = { val: 1, getVal: function() { var val = 2; return this.val; } }; console.log(testObj.getVal()); // 1
上述代码运行输出 1
, 顺藤摸瓜,getVal()
函数的调用者是 testObj
对象, 按照 this 指向调用该函数的对象
的原则,getVal()
中的 this 指向 testObj
对象, testObj
对象的 val
值是 1.
3) 构造函数
'use strict'; function testFunc(val) { this.a = val; this.b = 'bb'; } var testInstance = new testFunc('aa'); console.log(testInstance.a); // aa console.log(testInstance.b); // bb
当一个函数的调用者是构造函数(new 出来的对象), this 指向新构造出来的对象 testInstance
4) call and apply
通过 call apply 将 this 指向特定对象:
function testFunc(val) { this.a = val; this.b = 'bb'; } function execFunc() { var a = 'exec aa'; var b = 'exec bb'; console.log(this.a, this.b); } var testInstance = new testFunc('aa'); execFunc.call(testInstance); // aa bb execFunc.apply(testInstance); // aa bb
通过 call apply 函数将 execFunc 的 this 值指向 testInstance 对象的 this 值。
注意: 以
fun.apply() // or call
为例 call apply 的第一个参数是func 函数运行时的 this 值
(第一个参数的解释版本真的多)。二者的区别这里不说。
补充:箭头函数、严格模式下的 this
1) ES6 中的箭头函数 arrow function
GLOBAL.a = 'global aa';var testObj = { a: 'aa', getValArrowFuc: function() { var val = (() => this.a); return val(); }, getVal: function() { var self = this; var val = function() { return self.a; }; return val(); }, getValGlobal: function() { var val = function() { return this.a; }; return val(); } }; console.log(testObj.getValArrowFuc()); // aa console.log(testObj.getVal()); // aa console.log(testObj.getValGlobal()); // global aa
箭头函数中的 this 值,就是词法作用域的 this 值。
2) 严格模式下的 this
对于一个开启严格模式的函数,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined.
'use strict';/** * from MDN */function fun() { return this; } console.log(fun() === undefined); // true console.log(fun.call(2) === 2); // true console.log(fun.apply(null) === null); // true console.log(fun.call(undefined) === undefined); // true console.log(fun.bind(true)() === true); // true
注: 以上所有不考虑 Eval
闭包 closure
闭包的构成:
- 函数
- 创建该函数的环境,环境由闭包创建时在作用域中的任何局部变量组成
自执行函数表达式写法:
var test = (function() { var val = 0; var add = function(num) { val += num; return val; }; return add; })(); console.log(test(3)); // 3 console.log(test(4)); // 7
个人感觉这个写法可读性更好:
var test = function() { var val = 0; var add = function(num) { val += num; return val; }; return add; }; /** * 此处 instance 是一个闭包。 * 由 add 函数, 和创建 add 函数时的环境(变量 val)组成 */ var instance = test(); console.log(instance(3)); // 3 console.log(instance(4)); // 7
以下代码:
function test() { for (var i = 0; i < 5; i++) { setTimeout(() => { console.log(i); }, 10); } } test();
输出是:
5 5 5 5 5
这里变量 i 的作用域是 test 函数作用域,也就是说 console.log(i)
中的 i 是 test 函数作用域下的同一个变量。
setTimeout 中的函数被执行时,for 遍历已完成并且 i 被赋值为 5.
利用闭包:
function test() { for (var i = 0; i < 5; i++) { (function (val) { setTimeout(() => { console.log(val); }, 10); })(i); } } test();
则会输出:
0 1 2 3 4
这里我们将 i 赋值成一个局部变量,可在闭包内访问(每次循环创建一个闭包, i 作为该闭包作用域下的局部变量,不跟随外层 i 的值改变)。
闭包对性能有负面影响(尤其是内存占用),如果不需要使用,则不使用。