软件开发架构师

var、let和const应该怎么用?

前端 76 2019-03-11 11:16

在这篇文章中,我们将介绍两种新的在 JavaScript(ES6)中创建变量的方式,即使用 let 和 const。同时,我们将探讨 var、let 和 const 之间的区别,以及函数与块作用域、变量提升和不变性等内容。

如果你想观看视频,这是链接:https://youtu.be/6vBYfLCE9-Q

ES2015(或 ES6)引入了两种创建变量的新方法:let 和 const。但在我们深入了解 var、let 和 const 之间的区别之前,需要先了解一些先决条件。它们是变量声明与初始化、作用域(特别是函数作用域)和变量提升。

变量声明与初始化

变量声明引入了新标识符。

复制代码
var declaration

我们创建了一个叫作 declaration 的新标识符。在 JavaScript 中,刚创建的变量会被初始化为 undefined。如果我们在控制台打印变量 declaration,将会看到输出 undefined。

复制代码
var declaration 
console.log(declaration)

与变量声明相反,变量初始化是指首次为变量赋值。

复制代码
var declaration
console.log(declaration) // undefined
declaration = 'This is an initialization'

我将一个字符串赋值给 declaration 变量,以此来初始化它。

这引出了我们的第二个概念——作用域。

作用域

作用域定义了在程序内部可以访问哪里的变量和函数。JavaScript 中有两种作用域——全局作用域和函数作用域。官方规范中提到:

“如果变量语句出现在函数声明中,那么变量的作用域就是函数的局部作用域。”

也就是说,如果你使用 var 创建一个变量,那么该变量被“限定”在创建这个变量的函数中,并且只能在该函数或其他嵌套函数内部访问它。

复制代码
function getDate () {
  var date = new Date()
  return date
}
getDate()
console.log( date) // ❌ Reference Error

在上面的代码中,我们尝试在函数外部访问一个在函数内部声明的变量。因为 date 作用域被限定在 getDate 函数内部,所以它只能在 getDate 内部或 getDate 中的嵌套函数中访问(如下所示)。

复制代码
function getDate () {
  var date = new Date()
  function formatDate () {
    return date.toDateString().slice( 4) // ✅ 
  }
  return formatDate()
}
getDate()
console.log( date) // ❌ Reference Error

现在,让我们来看一个更高级的例子。假设我们有一个 prices 数组,我们需要一个函数,将这个数组和 discount 作为参数,并返回一个新的折扣价数组。最终的函数可能如下所示:

复制代码
discountPrices([ 100, 200, 300], .5)

函数的实现可能看起来像这样:

复制代码
function discountPrices (prices, discount) {
  var discounted = []
  for (var i = 0; i < prices. length; i++) {
    var discountedPrice = prices[ i] * ( 1 - discount)
    var finalPrice = Math. round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  return discounted
}

看起来很简单,但这与块作用域有什么关系?看一下 for 循环,在其中声明的变量是否可以在外部访问?事实证明,这样是可以的。

复制代码
function discountPrices (prices, discount) {
  var discounted = []
  for ( var i = 0; i < prices.length; i++) {
    var discountedPrice = prices[i] * ( 1 - discount)
    var finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}

如果 JavaScript 是你所知道的唯一编程语言,那么你可能不会多想什么。但是,如果你从其他编程语言(特别是具有块作用域的编程语言)转到 JavaScript,那么你可能会对这个问题感到奇怪。

你可能感到很奇怪,因为我们没有理由在 for 循环之外还能继续访问 i、discountedPrice 和 finalPrice。它对我们没有任何好处,甚至在某些情况下可能会对我们造成伤害。但因为用 var 声明的变量的作用域是整个函数,所以你可以这样做。

现在,我们已经讨论完变量声明、初始化和作用域,在深入了解 let 和 const 之前,我们需要了解的最后一个东西是变量提升。

变量提升

之前我们说过,“在 JavaScript 中,刚创建的变量会被初始化为 undefined”。事实证明,这就是“变量提升”。JavaScript 解释器将在所谓的“创建”阶段为声明的变量分配默认值 undefined。

有关变量创建、提升和作用域的更多内容,请参阅:https://tylermcginnis.com/ultimate-guide-to-execution-contexts-hoisting-scopes-and-closures-in-javascript/

让我们继续前面的例子,看看变量提升是如何影响它的。

复制代码
function discountPrices (prices, discount) {
  var discounted = undefined
  var i = undefined
  var discountedPrice = undefined
  var finalPrice = undefined
  discounted = []
  for ( var i = 0; i < prices.length; i++) {
    discountedPrice = prices[i] * ( 1 - discount)
    finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}

请注意,所有声明的变量都被分配了默认值 undefined。这就是为什么如果你在实际声明之前尝试访问其中的一个变量,就会得到 undefined。

复制代码
function discountPrices (prices, discount) {
  console.log(discounted) // undefined
  var discounted = []
  for ( var i = 0; i < prices.length; i++) {
    var discountedPrice = prices[i] * ( 1 - discount)
    var finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}

接下来让我们来讨论我们的重点:var、let 和 const 之间的区别。

var、let 和 const

首先,我们先来比较 var 和 let。var 和 let 之间的主要区别在于,let 不是函数作用域的,而是块作用域的。这意味着使用 let 关键字创建的变量可以在创建它的“块”内以及嵌套块内访问。这里所说的“块”是指用大括号{}包围的任何东西,比如 for 循环或 if 语句。

让我们再回顾一下我们的 discountPrices 函数。

复制代码
function discountPrices (prices, discount) {
  var discounted = []
  for ( var i = 0; i < prices.length; i++) {
    var discountedPrice = prices[i] * ( 1 - discount)
    var finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}

还记得吗,我们可以在 for 循环之外访问 i、discountedPrice 和 finalPrice,因为它们是用 var 声明的,而 var 是函数作用域的。但是现在,如果我们将 var 声明更改为 let 并运行它,会发生什么?

复制代码
function discountPrices (prices, discount) {
  let discounted = []
  for ( let i = 0; i < prices.length; i++) {
    let discountedPrice = prices[i] * ( 1 - discount)
    let finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}
discountPrices([ 100, 200, 300], .5) // ❌ ReferenceError: i is not defined

我们会得到 ReferenceError: i is not defined 错误。这告诉我们,使用 let 声明的变量是块作用域的,而不是函数作用域。因此,试图在“块”之外访问 i(或者 discountedPrice 和 finalPrice)将会得到一个很少见的错误。

下一个区别与变量提升有关。之前我们曾说过,变量提升是指“JavaScript 解释器会在所谓的创建阶段将声明的变量赋值为默认值 undefined”。

复制代码
function discountPrices (prices, discount) {
  console.log(discounted) // undefined
  var discounted = []
  for ( var i = 0; i < prices.length; i++) {
    var discountedPrice = prices[i] * ( 1 - discount)
    var finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}

我想不出任何你需要在声明变量之前访问变量的理由,所以,看来抛出一个 ReferenceError 错误比返回 undefined 会更好。

事实上,这正是 let 所做的事情。如果你尝试在声明之前访问使用 let 声明的变量,你将得到 ReferenceError 错误,而不是 undefined。

复制代码
function discountPrices (prices, discount) {
  console.log(discounted) // ❌ ReferenceError
  let discounted = []
  for ( let i = 0; i < prices.length; i++) {
    let discountedPrice = prices[i] * ( 1 - discount)
    let finalPrice = Math.round(discountedPrice * 100) / 100
    discounted.push(finalPrice)
  }
  console.log(i) // 3
  console.log(discountedPrice) // 150
  console.log(finalPrice) // 150
  return discounted
}

let 与 const

你已经理解了 var 和 let 之间的区别,那么 const 呢?事实证明,const 与 let 几乎完全相同。但是,唯一的区别是,一旦使用 const 为变量赋值,就无法对其重新赋值。

复制代码
let name = 'Tyler'
const handle = 'tylermcginnis'
name = 'Tyler McGinnis' // ✅
handle = '@tylermcginnis' // ❌ TypeError: Assignment to constant variable.

在上面代码中,用 let 声明的变量可以重新赋值,但用 const 声明的变量不能。

所以,如果你想要让一个变量不可变,可以用 const 来声明它。但其实不然,只是用 const 声明变量并不意味着它是不可变的,只是无法对其重新赋值而已。请看下面的例子。

复制代码
const person = {
  name: 'Kim Kardashian'
}
person.name = 'Kim Kardashian West' // ✅
person = {} // ❌ Assignment to constant variable.

请注意,修改对象的属性并不会导致重新赋值,所以,即使使用 const 来声明对象,并不意味着你不能修改它的属性。它只是表示你无法为其重新赋值。

但我们还没有回答最重要问题:你应该使用 var、let 还是 const?流行的观点认为,除非你知道变量会发生变化,否则应该总是使用 const。因为这是在向未来的自己以及未来会阅读你的代码的其他开发者发出信号:不应该修改这些变量。如果它需要被修改(比如在 for 循环中),你应该使用 let。

因此,在会发生变化的变量和不会发生变化的变量之间,剩下的就不多了。这意味着你不应该再使用 var。

同时也存在一个不是很流行的观点,就是永远不应该使用 const,因为即使你试图表明变量是不可变的,正如我们上面所看到的那样,情况并非完全如此。接受这个意见的开发者总是使用 let,除非他们的变量是常量,例如_LOCATION_等。

这里再回顾一下,var 是函数作用域的,如果你尝试在声明之前访问用 var 声明的变量,就会得到 undefined。const 和 let 是块作用域的,如果你尝试在声明之前访问用 let 或 const 声明的变量,你会得到一个 ReferenceError 错误。最后,let 和 const 之间的区别在于,一旦你为用 const 声明的变量赋值,你就不能重新赋值,但如果变量是用 let 声明的,是可以重新赋值的。

更多内容,请关注前端之巅。

英文原文:https://medium.freecodecamp.org/var-vs-let-vs-const-in-javascript-2954ae48c037

文章评论