前端那些事儿

[深入理解ES6]块级绑定

2019-08-08  本文已影响3人  LM林慕

var声明与变量提升

变量提升(hoisting):使用var关键字声明的变量,无论声明位置在何处,都会被视为声明于所在函数的顶部(如果声明不在任意函数内,则视为在全局作用域的顶部)。

function getValue(condition) {
  if (condition) {
    var value = "blue";
    // 其他代码
    return value;
  } else {
    // value 在此处可访问,值为 undefined
    return null;
  }
    // value 在此处可访问,值为 undefined
}

刚开始,你可能会认为仅当condition的值为true时,变量value才会被创建。但实际上,JS引擎在后台对getValue进行了调整,像这样:

function getValue(condition) {
  var value;
  if (condition) {
    value = "blue";
    // 其他代码
    return value;
  } else {
    return null;
  }
}

如上代码,value变量的声明被提升到了顶部,而初始化工作则保留在原处。
为了解决这个问题,ES6引入了块级作用域,让变量的生命周期更加可控。

块级声明

块级声明 :让所声明的变量在指定块的作用域外无法被访问。块级作用域(词法作用域)在如下情况被创建:

  1. 在一个函数内部
  2. 在一个代码块({})内部

let声明

同样是上面的代码范例:

function getValue(condition) {
  if (condition) {
    let value = "blue";
    // 其他代码
    return value;
  } else {
    // value 在此处不可用
    return null;
  }
  // value 在此处不可用
}

let声明不会将变量提升到函数顶部。

禁止重复声明

如果一个标识符已经在代码块内部被定义,那么在此代码块内使用同一个标识符进行let声明会抛出错误。eg:

var a=20
// 语法错误
let a=200

然而在嵌套的作用域内使用let声明一个同名变量是正常的:

var a=10
// 不会抛出错误
if (condition) {
  let a=100
  // 其他代码
}

未报错的原因是,在if代码块内部,这个新变量会屏蔽全局的a变量,从而在局部阻止对于后者的访问。

常量声明

使用 const 声明的变量会被认为是常量(constant),所有的 const 变量都需要在声明时进行初始化,eg:

// 有效的常量
const hi=30

// 语法错误:未进行初始化
const hi

对比常量声明与 let 声明

1.常量声明与 let 声明一样,都是块级声明。即在语法块外无法访问,声明也不会被提升;

if (condition) {
  const maxItems = 5;
  // 其他代码
}
// maxItems 在此处无法访问

2.const 声明会在同一个作用域(全局或是函数作用域)内定义一个已有变量时抛出错误;

var message = "Hello!";
let age = 25;
// 二者均会抛出错误
const message = "Goodbye!";
const age = 30;

3.对之前用 const 声明的常量进行赋值会抛出错误,无论严格模式还是非严格模式;

const maxItems = 5;
maxItems = 6; // 抛出错误

与其他语言的常量类似, maxItems 变量不能被再次赋值。然而与其他语言不同, JS 的常量
如果是一个对象,它所包含的值是可以被修改的。

使用 const 声明对象

const person = {
  name: "Nicholas"
};
// 工作正常
person.name = "Greg";
// 抛出错误
person = {
  name: "Greg"
};

const阻止的是对于变量绑定的修改,而不阻止对成员值的修改。

暂时性死区

if(condition){
  console.log(typeof value); // 引用错误
  let value = 'blue';
}

value位于被JS社区称为暂时性死区(temporal dead zone,TDZ)的区域内,替换为 const 会有相同情况。
当JS引擎检视接下来的代码块并发现变量声明时,它会在面对var的情况下将声明提升到函数或全局作用域的顶部,而面对letconst时会将声明放在暂时性死区内。只有执行到变量声明语句时,变量才可安全使用,否则报runtime error
对比以下代码:

console.log(typeof value); // undefined
if(condition){
  left value = 'blue';
}

为什么和上面的结果不一样呢?
因为当typeof运算符被使用时,value并没有在暂时性死区内。

循环中的块级绑定

请对比以下代码

for (var i = 0; i < 10; i++) {
  process(items[i]);
}
// i 在此处仍然可被访问
console.log(i); // 10
for (let i = 0; i < 10; i++) {
  process(items[i]);
}
// i 在此处不可访问,抛出错误
console.log(i);

这一次欣赏到了变量提升的魔性吗!

循环内的函数

var funcs = [];

for (var i = 0; i < 10; i++) {
  funcs.push(function() { console.log(i); });
}

funcs.forEach(function(func) {
  func();  // 输出数值‘10’十次
});

你原本可能预期这段代码会输出 09 的数值,但它却在同一行将数值 10 输出了十次。这是因为变量 i在循环的每次迭代中都被共享了,意味着循环内创建的那些函数都拥有对于同一变量的引用。在循环结束后,变量 i 的值会是 10 ,因此当console.log(i)被调用时,每次都打印出 10。循环内使用立即调用函数表达式(IIFEs)解决这个问题。

var funcs = [];
for (var i = 0; i < 10; i++) {
  funcs.push((function(value) {
    return function() {
      console.log(value);
    }
  }(i)));
}
funcs.forEach(function(func) {
  func(); // 从 0 到 9 依次输出
});

这种写法在循环内使用了 IIFE 。变量i被传递给 IIFE ,从而创建了value 变量作为自身副本并将值存储于其中。 value 变量的值被迭代中的函数所使用,因此在循环从 09 的过程中调用每个函数都返回了预期的值。更简单的方法请往下看:

循环内的let声明

var funcs = [];
for (let i = 0; i < 10; i++) {
  funcs.push(function() {
    console.log(i);
  });
}
funcs.forEach(function(func) {
  func(); // 从 0 到 9 依次输出
})

在循环中,let 声明每次都创建了一个新的 i 变量,因此在循环内部创建的函数获得了各自的 i 副本,而每个 i 副本的值都在每次循环迭代声明变量的时候被确定了。这种方式在 for-infor-of 循环中同样适用,如下所示:

var funcs = [],
object = {
    a: true,
    b: true,
    c: true
};
for (let key in object) {
  funcs.push(function() {
    console.log(key);
  });
}
funcs.forEach(function(func) {
  func(); // 依次输出 "a"、 "b"、 "c"
});

如果使用var 来声明 key ,则所有函数都只会输出 "c"

需要重点了解的是: let 声明在循环内部的行为是在规范中特别定义的,而与不提升变量声明的特征没有必然联系。事实上,在早期 let 的实现中并没有这种行为,它是后来才添加的。

循环内的常量声明

var funcs = [];
// 在一次迭代后抛出错误
for (const i = 0; i < 10; i++) {
  funcs.push(function() {
    console.log(i);
  });
}

在此代码中,i 被声明为一个常量。循环的第一次迭代成功执行,此时 i 的值为 0 。在i++ 执行时,一个错误会被抛出,因为该语句试图更改常量的值。

var funcs = [],
object = {
  a: true,
  b: true,
  c: true
};
// 不会导致错误
for (const key in object) {
  funcs.push(function() {
    console.log(key);
  });
}
funcs.forEach(function(func) {
  func(); // 依次输出 "a"、 "b"、 "c"
});

const 能够在 for-infor-of 循环内工作,是因为循环为每次迭代创建了一个新的变量绑定,而不是试图去修改已绑定的变量的值。

全局块级绑定

// 在浏览器中
var RegExp = "Hello!";
console.log(window.RegExp); // "Hello!"
var ncz = "Hi!";
console.log(window.ncz); // "Hi!"

尽管全局的 RegExp 是定义在 window 上的,它仍然不能防止被 var 重写。这个例子声明了一个新的全局变量 RegExp 而覆盖了原有对象。类似的, ncz 定义为全局变量后就立即成为了 window 的一个属性。这就是 JS 通常的工作方式。

// 在浏览器中
let RegExp = "Hello!";
console.log(RegExp); // "Hello!"
console.log(window.RegExp === RegExp); // false
const ncz = "Hi!";
console.log(ncz); // "Hi!"
console.log("ncz" in window); // false

若想让代码能从全局对象中被访问,你仍然需要使用 var。在浏览器中跨越帧或窗口去访问代码时,这种做法非常普遍。

块级绑定新的最佳实践&&总结

块级绑定当前的最佳实践就是:在默认情况下使用 const ,而只在你知道变量值需要被更改的情况下才使用let 。这在代码中能确保基本层次的不可变性,有助于防止某些类型的错误。

上一篇 下一篇

猜你喜欢

热点阅读