JavaScript 变量作用域、this、闭包

JavaScript 变量作用域、this、闭包

变量作用域 scope 和作用域链 scope chain

var 变量的作用域和变量提升

JavaScript has two scopes: global and local.
A variable that is declared outside a function definition is a global variable, and its value is accessible and modifiable throughout your program.
A variable that is declared inside a function definition is local.
It is created and destroyed every time the function is executed, and it cannot be accessed by any code outside the function.
JavaScript does not support block scope (in which a set of braces {. . .} defines a new scope), except in the special case of block-scoped variables.

JavaScript 有两种作用域:全局和局部。
在函数定义之外声明的变量是全局变量,它的值可在整个程序中访问和修改。
在函数定义内声明的变量是局部变量。每当执行函数时,都会创建和销毁该变量,且无法通过函数之外的任何代码访问该变量。

JavaScript 中,在函数体内 var 声明的变量是函数级作用域,是局部变量,在本函数体内可以访问,而且是在函数体内任意位置可以访问。

  1. // JS 代码
  2. function test() {
  3.     console.log(val);
  4.     var val = 'this is val';
  5.     console.log(val);
  6.     func();
  7.     function func() {
  8.         for (var i = 0; i < 5; i++) {
  9.         }
  10.         console.log('i: ', i);
  11.         console.log('this is func');
  12.     }
  13. }
  14. test();

上述代码结果是:

  1. undefined
  2. this is val
  3. i:  5
  4. this is func

JavaScript 解析器预解析代码的时候, test 函数作如下解析:

  1. function test() {
  2.     // 变量提升, 缺省值是 undefined
  3.     var val;
  4.     // 函数声明提升
  5.     function func() {
  6.         // 变量提升
  7.         var i;
  8.         for (i = 0, i < 5, i++) {
  9.         }
  10.         console.log('i: ', i);
  11.         console.log('this is func');
  12.     }
  13.     console.log(val);
  14.     // 变量赋值
  15.     val = 'this is val';
  16.     console.log(val);
  17.     func();
  18. }

所以第一次 console.log(val) 时候并不会抛异常, 因为此时变量 val 是被声明过的,值是 undefined。
理解 变量提升 ,写代码时应注意 变量污染 的坑。

如果声明变量时不加 var 直接 val = 1;, 那么 val 是全局变量。

作用域链

作用域链包含了执行环境有权访问的所有变量和访问顺序。

作为单线程语言的 JavaScript,初始化代码时会创建一个全局上下文,每一次函数调用都会创建一个执行上下文,执行上下文及包含关系:

·变量对象
·变量
·函数声明
·参数(arguments)
·作用域链
·有权访问的变量和访问顺序(本作用域变量和所有父作用域变量)。即函数内部属性 scope : 本函数有权访问的[变量、对象、函数]的集合
·this 值
如下代码:

  1. function func_1() {
  2.     var val_1 = 1;
  3.     // 抛异常: ReferenceError: val_2 is not defined
  4.     console.log(val_1, val_2);
  5.     function func_2() {
  6.         var val_2 = 2;
  7.         // 输出:1 2                                     
  8.         console.log(val_1, val_2);
  9.     }
  10.     func_2();
  11. }
  12. 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 声明的变量作用域是代码块,这个设计比较符合大多数人的思维方式。(代码块简单来说就是 {} 大括号包着的区域)

  1. function test() {
  2.     if (true) {
  3.         var a = 'a';
  4.         let b = 'b';
  5.     }
  6.     // 输出: a
  7.     console.log(a);
  8.     // 抛异常:ReferenceError: b is not defined
  9.     console.log(b);
  10. }
  11. test();

关于 const 的作用有必要正确理解:
严格来说, const 声明了一个指向变量的指针,并不是说 const 声明的变量不可改变, 而是该指针指向的地址不可改变。

MDN 的例子很赞,这里直接拷过来看:

  1. // NOTE: Constants can be declared with uppercase or lowercase, but a common
  2. // convention is to use all-uppercase letters.
  3. // define MY_FAV as a constant and give it the value 7
  4. const MY_FAV = 7;
  5. // this will fail silently in Firefox and Chrome (but does not fail in Safari)
  6. MY_FAV = 20;
  7. // will print 7
  8. console.log("my favorite number is: " + MY_FAV);
  9. // trying to redeclare a constant throws an error 
  10. const MY_FAV = 20;
  11. // the name MY_FAV is reserved for constant above, so this will also fail
  12. var MY_FAV = 20;
  13. // MY_FAV is still 7
  14. console.log("my favorite number is " + MY_FAV);
  15. // Assigning to A const variable is a syntax error
  16. const A = 1; A = 2;
  17. // const requires an initializer
  18. const FOO; // SyntaxError: missing = in const declaration
  19. // const also works on objects
  20. const MY_OBJECT = {"key""value"};
  21. // Overwriting the object fails as above (in Firefox and Chrome but not in Safari)
  22. MY_OBJECT = {"OTHER_KEY""value"};
  23. // However, object keys are not protected,
  24. // so the following statement is executed without problems
  25. MY_OBJECT.key = "otherValue"// Use Object.freeze() to make object immutable

this

In most cases, the value of this is determined by how a function is called.
It can't be set by assignment during execution, and it may be different each time the function is called.

简言之: this 总是指向调用该函数的对象。

全局上下文 global context
  1. // 在浏览器中
  2. console.log(this === window); // true
函数上下文 function context

在函数中访问 this 时, this 指向调用该函数的对象。

1)全局对象

  1. // 全局变量
  2. val = 1;
  3. function test() {
  4.     console.log(this.val);
  5. }
  6. test(); // 1

上例中,调用 test 函数的对象并不是一个自己声明的函数或对象,此时 this 默认值为全局对象。
2) 调用对象

  1. var testObj = {
  2.     val: 1,
  3.     getVal: function() {
  4.         var val = 2;
  5.         return this.val;
  6.     }
  7. };
  8. console.log(testObj.getVal()); // 1

上述代码运行输出 1, 顺藤摸瓜,getVal() 函数的调用者是 testObj 对象, 按照 this 指向调用该函数的对象 的原则, getVal() 中的 this 指向 testObj 对象, testObj 对象的 val 值是 1.
3) 构造函数

  1. 'use strict';
  2. function testFunc(val) {
  3.     this.a = val;
  4.     this.b = 'bb';
  5. }
  6. var testInstance = new testFunc('aa');
  7. console.log(testInstance.a); // aa
  8. console.log(testInstance.b); // bb

当一个函数的调用者是构造函数(new 出来的对象), this 指向新构造出来的对象 testInstance

4) call and apply
通过 call apply 将 this 指向特定对象:

  1. function testFunc(val) {
  2.     this.a = val;
  3.     this.b = 'bb';
  4. }
  5. function execFunc() {
  6.     var a = 'exec aa';
  7.     var b = 'exec bb';
  8.     console.log(this.a, this.b);
  9. }
  10. var testInstance = new testFunc('aa');
  11. execFunc.call(testInstance); // aa bb
  12. execFunc.apply(testInstance); // aa bb

通过 call apply 函数将 execFunc 的 this 值指向 testInstance 对象的 this 值。

注意: 以 fun.apply() // or call 为例 call apply 的第一个参数是 func 函数运行时的 this 值 (第一个参数的解释版本真的多)。
二者的区别这里不说。

补充:箭头函数、严格模式下的 this

1) ES6 中的箭头函数 arrow function

An arrow function expression has a shorter syntax compared to function expressions and lexically binds the this value
(does not bind its own this, arguments, super, or new.target).
Arrow functions are always anonymous.

  1. GLOBAL.a = 'global aa';
  2. var testObj = {
  3.     a: 'aa',
  4.     getValArrowFuc: function() {
  5.         var val = (() => this.a);
  6.         return val();
  7.     },
  8.     getVal: function() {
  9.         var self = this;
  10.         var val = function() {
  11.             return self.a;
  12.         };
  13.         return val();
  14.     },
  15.     getValGlobal: function() {
  16.         var val = function() {
  17.             return this.a;
  18.         };
  19.         return val();
  20.     }
  21. };
  22. console.log(testObj.getValArrowFuc()); // aa
  23. console.log(testObj.getVal()); // aa
  24. console.log(testObj.getValGlobal()); // global aa

箭头函数中的 this 值,就是词法作用域的 this 值。

2) 严格模式下的 this

对于一个开启严格模式的函数,指定的 this 不再被封装为对象,而且如果没有指定 this 的话它值是 undefined.

  1. 'use strict';
  2. /**
  3.  * from MDN
  4.  */
  5. function fun() { return this; }
  6. console.log(fun() === undefined); // true
  7. console.log(fun.call(2) === 2); // true
  8. console.log(fun.apply(null) === null); // true
  9. console.log(fun.call(undefined) === undefined); // true
  10. console.log(fun.bind(true)() === true); // true

注: 以上所有不考虑 Eval

闭包 closure

闭包的构成:

Closures are functions whose inner functions refer to independent (free) variables.
In other words, the functions defined in the closure 'remember' the environment in which they were created.

函数
创建该函数的环境,环境由闭包创建时在作用域中的任何局部变量组成

自执行函数表达式写法:

  1. var test = (function() {
  2.     var val = 0;
  3.     var add = function(num) {
  4.         val += num;
  5.         return val;
  6.     };
  7.     return add;
  8. })();
  9. console.log(test(3)); // 3
  10. console.log(test(4)); // 7

个人感觉这个写法可读性更好:

  1. var test = function() {
  2.     var val = 0;
  3.     var add = function(num) {
  4.         val += num;
  5.         return val;
  6.     };
  7.     return add;
  8. };
  9. /**
  10.  * 此处 instance 是一个闭包。
  11.  * 由 add 函数, 和创建 add 函数时的环境(变量 val)组成
  12.  */
  13. var instance = test();
  14. console.log(instance(3)); // 3
  15. console.log(instance(4)); // 7

以下代码:

  1. function test() {
  2.     for (var i = 0; i < 5; i++) {
  3.         setTimeout(() => {
  4.             console.log(i);
  5.         }, 10);
  6.     }
  7. }
  8. test();

输出是:

  1. 5
  2. 5
  3. 5
  4. 5
  5. 5

这里变量 i 的作用域是 test 函数作用域,也就是说 console.log(i) 中的 i 是 test 函数作用域下的同一个变量。

setTimeout 中的函数被执行时,for 遍历已完成并且 i 被赋值为 5.
利用闭包:

  1. function test() {
  2.     for (var i = 0; i < 5; i++) {
  3.         (function (val) {
  4.             setTimeout(() => {
  5.                 console.log(val);
  6.             }, 10);
  7.         })(i);
  8.     }
  9. }
  10. test();

则会输出:

  1. 0
  2. 1
  3. 2
  4. 3
  5. 4

这里我们将 i 赋值成一个局部变量,可在闭包内访问(每次循环创建一个闭包, i 作为该闭包作用域下的局部变量,不跟随外层 i 的值改变)。

闭包对性能有负面影响(尤其是内存占用),如果不需要使用,则不使用。

虚拟主机
《JavaScript从入门到精通》PDF
《JavaScript DOM编程艺术》(第二版)PDF
《编写高质量JavaScript代码的68个有效方法》PDF
《你不知道的Javascript(上卷)》PDF
广告也精彩