前言

万丈高楼平地起,学习基础很重要。

前置知识

执行环境

执行环境(execution context)是 JavaScript 中最为重要的一个概念。执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。虽然我们编写的代码无法访问这个对象,但解析器在处理数据时会在后台使用它。

每个执行环境都有一个执行环境对象。

全局执行环境

全局执行环境是最外围的一个执行环境。在 Web 浏览器中,全局执行环境被认为是 window 对象。

全局执行环境直到应用程序退出,例如关闭网页或浏览器时才会被销毁。

函数执行环境

每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。

在函数执行之后,栈将其环境弹出,把控制权返回给之前的执行环境。ECMAScript 程序中的执行流正是由这个方便的机制控制着。

作用域

作用域可以分为:

  • 全局作用域
  • 局部作用域
  • 块级作用域(es6)

1.全局作用域

全局变量拥有全局作用域。变量和函数会挂载到 window 对象上。

var scope = 'global'; // 声明一个全局变量
function checkScope () {
  var scope = 'local'; // 声明一个同名局部变量
  myscope = 'local';
  return scope;        // 返回局部变量的值, 而不是全局变量的值
}

window.myscope // undefined => checkScope() 还未执行,该变量未声明
checkScope();  // local
window.myscope // local

2.局部作用域

局部变量是局部作用域,仅在函数体内有用。

var scope = 'global';
function checkScope () {
  var scope = 'local';
  function nested() {
    var scope = 'nested';
    return scope;
  }
  return nested();
}

checkScope() // nested =>返回的是 nested() 的 scope 
window.scope  // global => 全局变量 scope 并未被覆盖

3. 作用域优先级

函数体内局部变量优先级高于同名全局变量。同名全局变量会被覆盖。

scope = 'global';    // 声明一个全局变量,可以不用 var 声明
function checkScope2 () {
  scope = 'local';   // 修改了全局变量 scope
  myscope = 'local'; // 显式声明了一个新的全局变量
  return [scope, myscope];
}

checkScope2(); // [local, local] 
window.scope; // local => 全局变量修改了
window.myscope; // local =>全局命名空间搞乱了

4. 同名变量

var 声明变量会提升,内部变量可能会覆盖外层变量

var tmp = '哈哈';

function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}
f(); // undefined => 理想情况应该输出值 “ 哈哈 ”

原因在于,预编译后,if 语句内的 temp 声明提升了

var tmp = '哈哈';

function f() {
  var tmp 
  console.log(tmp); // 打印的是 if 里面提升 temp
  if (false) {
    tmp = 'hello world';
  }
}
f();

5.ES5模拟块级作用域

ES5 没有块级作用域。使用不当会造成变量泄露。

for (var k = 0; k < 5; k++) {
  setTimeout(function () {
      console.log('inside', k);
  }, 1000);
}
   
console.log('outside', k); // outside 5  => 理想情况下,k 仅在 for 循环中有效,这里不应该输出 5,应该提示 k is not defined
// 间隔1s,分别输出5个 inside 5 => 理想情况下,应该输出 0 1 2 3 4

window.k; // 5 => 可看出 k 是全局变量,所以当执行 for 里面的语句时,k已经循环完了5次,此时 k = 5

再来一题

var test = function() {
  var arr = [];
  for (var i = 0; i < 3; i++) {
    console.log('开始循环了', i)
    arr[i] = function() {
      return i * i;
    };
  }
  return arr;
};
 
var a = test(); // 输出 “开始循环了 0 1 2” => 此时 arr[i]是还未执行的,i 已经等于 3 了
a[1](); // 9
a[2](); // 9

块级作用域

通过前面的介绍,可以知道,ES5 是没有块级作用域的。变量使用不当,容易造成变量泄露,出现很多不合理场景。为了避免这种情况出现,我们可以使用以下方法:

以下都是针对ES5 没有块级作用域。使用不当会造成不合理场景列举的例子进行修改。

1. 借助立即执行函数

for (var k = 0; k < 5; k++) { 
  (function(k){
    //这里是块级作用域
    setTimeout(function (){
      console.log('inside', k);
     },1000);
  })(k);
}

console.log('outside', k);
// 输出 outside 5
// 再依次输出 inside 0 1 2 3 4

2. 定义函数并传值

var _loop = function _loop(k) {
  //这里是块级作用域
  setTimeout(function () {
    console.log(k);
  }, 1000);
};

for (var k = 0; k < 5; k++) {
  _loop(k);
}
// 依次输出 0 1 2 3 4 

1、2 写法都是利用了 JS 中调用函数传递参数都是值传递的特点

3. 使用setTimeout的第三个参数

for (let k = 0; k < 5; k++) {
  setTimeout(function () {
      console.log(k);
  }, 1000, k);
}
// 依次输出 0 1 2 3 4

4. 使用 let、const 声明变量

for (let k = 0; k < 5; k++) {
  setTimeout(function () {
      console.log(k);
  }, 1000);
}
   
console.log(k); // k is not defined
// 间隔1s,分别输出inside 0 1 2 3 4
关注执行顺序

作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。

  • 作用域链的用途,是保证对执行环境有权访问的所有变量和函数的有序访问。
  • 作用域链的前端,始终都是当前执行的代码所在环境的变量对象。如果这个环境是函数,则将其活动对象(activation object)作为变量对象。活动对象在最开始时只包含一个变量,即 arguments 对象(这个对象在全局环境中是不存在的)。
  • 作用域链中的下一个变量对象来自包含(外部)环境,而再下一个变量对象则来自下一个包含环境。这样,一直延续到全局执行环境。
  • 全局执行环境的变量对象始终都是作用域链中的最后一个对象。

上面的说明是从《JavaScript权威指南》指南中摘抄出来的。

简单点总结就是:

当你使用一个变量时,会先从当前作用域找,如果找不到就往上找,一层一层往直到找到全局作用域都还没找到,就抛出错误,说明没有这个变量。这种一层一层的关系,就是作用域链 。

var a = 100
function F1() {
    var b = 200
    function F2() {
        var c = 300
        console.log(a) // 100 顺作用域链向父作用域找
        console.log(b) // 200 顺作用域链向父作用域找
        console.log(c) // 300 本作用域的变量
        console.log(d) // ReferenceError:d is not defined 找到window都找不到,变量不存在,报错
    }
    F2()
}
F1()

YanniLi
56 声望4 粉丝