资讯专栏INFORMATION COLUMN

ES2015系列--块级作用域

darkbug / 3407人阅读

摘要:在的闭包中,闭包函数能够访问到包庇函数中的变量,这些闭包函数能够访问到的变量也因此被称为自由变量。在之前最常见的两种作用域,全局作用局和函数作用域局部作用域。

关于文章讨论请访问:https://github.com/Jocs/jocs....

当Brendan Eich在1995年设计JavaScript第一个版本的时候,考虑的不是很周到,以至于最初版本的JavaScript有很多不完善的地方,在Douglas Crockford的《JavaScript:The Good Parts》中就总结了很多JavaScript不好的地方,比如允许!===的使用,会导致隐式的类型转换,比如在全局作用域中通过var声明变量会成为全局对象(在浏览器环境中是window对象)的一个属性,在比如var声明的变量可以覆盖window对象上面原生的方法和属性等。

但是作为一门已经被广泛用于web开发的计算机语言来说,去纠正这些设计错误显得相当困难,因为如果新的语法和老的语法有冲突的话,那么已有的web应用无法运行,浏览器生产厂商肯定不会去冒这个险去实现这些和老的语法完全冲突的功能的,因为谁都不想失去自己的客户,不是吗?因此向下兼容便成了解决上述问题的唯一途径,也就是说在不改变原有语法特性的基础上,增加一些新的语法或变量声明方式等,来把新的语言特性引入到JavaScript语言中。

早在九年前,Brendan Eich在Firefox中就实现了第一版的let.但是let的功能和现有的ES2015标准规定有些出入,后来由Shu-yu Guo将let的实现升级到符合现有的ES2015标准,现在才有了我们现在在最新的Firefox中使用的let 声明变量语法。

问题一:没有块级作用域

在ES2015之前,在函数中通过var声明的变量,不论其在{}中还是外面,其都可以在整个函数范围内访问到,因此在函数中声明的变量被称为局部变量,作用域被称为局部作用域,而在全局中声明的变量存在整个全局作用域中。但是在很多情境下,我们迫切的需要块级作用域的存在,也就是说在{}内部声明的变量只能够在{}内部访问到,在{}外部无法访问到其内部声明的变量,比如下面的例子:

function foo() {
    var bar = "hello"
    if (true) {
        var zar = "world"
        console.log(zar)
    }
    console.log(zar) // 如果存在块级作用域那么将报语法错误:Uncaught ReferenceError
}

在上面的例子中,如果JavaScript在ES2015之前就存在块级作用域,那么在{}之外将无法访问到其内部声明的变量zar,但是实际上,第二个console却打印了zar的赋值,"world"。

问题二:for循环中共享迭代变量值

在for循环初始循环变量时,如果使用var声明初始变量i,那么在整个循环中,for循环内部将共享i的值。如下代码:

var funcs = []
for (var i = 0; i < 10; i++) {
    funcs.push(function() {
        return i
    })
}
funcs.forEach(function(f) {
    console.log(f()) // 将在打印10数字10次
})

上面的代码并没有按着我们希望的方式执行,我们本来希望是最后打印0、1、2...9这10个数字。但是最后的结果却出乎我们的意料,而是将数字10打印了10次,究其原因,声明的变量i在上面的整个代码块能够访问到,也就是说,funcs数组中每一个函数返回的i都是全局声明的变量i。也就说在funcs中函数执行时,将返回同一个值,而变量i初始值为0,当迭代最后一次进行累加,9+1 = 10时,通过条件语句i < 10判断为false,循环运行完毕。最后i的值为10.也就是为什么最后所有的函数都打印为10。那么在ES2015之前能够使上面的循环打印0、1、2、… 9吗?答案是肯定的。

var funcs = []
for (var i = 1; i < 10; i++) {
    funcs.push((function(value) {
        return function() {
            return value
        }
    })(i))
}
funcs.forEach(function(f) {
    console.log(f())
})

在这儿我们使用了JavaScript中的两个很棒的特性,立即执行函数(IIFEs)和闭包(closure)。在JavaScript的闭包中,闭包函数能够访问到包庇函数中的变量,这些闭包函数能够访问到的变量也因此被称为自由变量。只要闭包没有被销毁,那么外部函数将一直在内存中保存着这些变量,在上面的代码中,形参value就是自由变量,return的函数是一个闭包,闭包内部能够访问到自由变量value。同时这儿我们还使用了立即执行函数,立即函数的作用就是在每次迭代的过程中,将i的值作为实参传入立即执行函数,并执行返回一个闭包函数,这个闭包函数保存了外部的自由变量,也就是保存了当次迭代时i的值。最后,就能够达到我们想要的结果,调用funcs中每个函数,最终返回0、1、2、… 9。

问题三:变量提升(Hoisting)

我们先来看看函数中的变量提升, 在函数中通过var定义的变量,不论其在函数中什么位置定义的,都将被视作在函数顶部定义,这一特定被称为提升(Hoisting)。想知道变量提升具体是怎样操作的,我们可以看看下面的代码:

function foo() {
    console.log(a) // undefined
    var a = "hello"
    console.log(a) // "hello"
}

在上面的代码中,我们可以看到,第一个console并没有报错(ReferenceError)。说明在第一个console.log(a)的时候,变量a已经被定义了,JavaScript引擎在解析上面的代码时实际上是像下面这样的:

function foo() {
  var a
  console.log(a)
  a = "hello"
  console.log(a)
}

也就是说,JavaScript引擎把变量的定义和赋值分开了,首先对变量进行提升,将变量提升到函数的顶部,注意,这儿变量的赋值并没有得到提升,也就是说a = "hello"依然是在后面赋值的。因此第一次console.log(a)并没有打印hello也没有报ReferenceError错误。而是打印undefined。无论是函数内部还是外部,变量提升都会给我们带来意想不到的bug。比如下面代码:

if (!("a" in window)) {
  var a = "hello"
}
console.log(a) // undefined

很多公司都把上面的代码作为面试前端工程师JavaScript基础的面试题,其考点也就是考察全局环境下的变量提升,首先,答案是undefined,并不是我们期许的hello。原因就在于变量a被提升到了最上面,上面的代码JavaScript其实是这样解析的:

var a
if (!("a" in window)) {
  a = "hello"
}
console.log(a) // undefined

现在就很明了了,bianlianga被提升到了全局环境最顶部,但是变量a的赋值还是在条件语句内部,我们知道通过关键字var在全局作用域中声明的变量将作为全局对象(window)的一个属性,因此"a" in windowtrue。所以if语句中的判断语句就为false。因此条件语句内部就根本不会执行,也就是说不会执行赋值语句。最后通过console.log(a)打印也就是undefined,而不是我们想要的hello

虽然使用关键词let进行变量声明也会有变量提升,但是其和通过var申明的变量带来的变量提升是不一样的,这一点将在后面的letvar的区别中讨论到。

关于ES2015之前作用域的概念

上面提及的一些问题,很多都是由于JavaScript中关于作用域的细分粒度不够,这儿我们稍微回顾一下ES2015之前关于作用域的概念。

Scope: collects and maintains a look-up list of all the declared identifiers (variables), and enforces a strict set of rules as to how these are accessible to currently executing code.

上面是关于作用域的定义,作用域就是一些规则的集合,通过这些规则我们能够查找到当前执行代码所需变量的值,这就是作用域的概念。在ES2015之前最常见的两种作用域,全局作用局和函数作用域(局部作用域)。函数作用域可以嵌套,这样就形成了一条作用域链,如果我们自顶向下的看,一个作用域内部可以嵌套几个子作用域,子作用域又可以嵌套更多的作用域,这就更像一个‘’作用域树‘’而非作用域链了,作用域链是一个自底向上的概念,在变量查找的过程中很有用的。在ES3时,引入了try catch语句,在catch语句中形成了新的作用域,外部是访问不到catch语句中的错误变量。代码如下:

try {
  throw new Error()
} catch(err) {
  console.log(err)
}
console.log(err) //Uncaught ReferenceError

再到ES5的时候,在严格模式下(use strict),函数中使用eval函数并不会再在原有函数中的作用域中执行代码或变量赋值了,而是会动态生成一个作用域嵌套在原有函数作用域内部。如下面代码:

"use strict"
var a = function() {
    var b = "123"
    eval("var c = 456;console.log(c + b)") // "456123"
    console.log(b) // "123"
    console.log(c) // 报错
}

在非严格模式下,a函数内部的console.log(c)是不会报错的,因为eval会共享a函数中的作用域,但是在严格模式下,eval将会动态创建一个新的子作用域嵌套在a函数内部,而外部是访问不到这个子作用域的,也就是为什么console.log(c)会报错。

通过let来声明变量

通过let关键字来声明变量也通过var来声明变量的语法形式相同,在某些场景下你甚至可以直接把var替换成let。但是使用let来申明变量与使用var来声明变量最大的区别就是作用域的边界不再是函数,而是包含let变量声明的代码块({})。下面的代码将说明let声明的变量只在代码块内部能够访问到,在代码块外部将无法访问到代码块内部使用let声明的变量。

if (true) {
  let foo = "bar"
}
console.log(foo) // Uncaught ReferenceError

在上面的代码中,foo变量在if语句中声明并赋值。if语句外部却访问不到foo变量,报ReferenceError错误。

letvar的区别
变量提升的区别

在ECMAScript 2015中,let也会提升到代码块的顶部,在变量声明之前去访问变量会导致ReferenceError错误,也就是说,变量被提升到了一个所谓的“temporal dead zone”(以下简称TDZ)。TDZ区域从代码块开始,直到显示得变量声明结束,在这一区域访问变量都会报ReferenceError错误。如下代码:

function do_something() {
  console.log(foo); // ReferenceError
  let foo = 2;
}

而通过var声明的变量不会形成TDZ,因此在定义变量之前访问变量只会提示undefined,也就是上文以及讨论过的var的变量提升。

全局环境声明变量的区别

在全局环境中,通过var声明的变量会成为window对象的一个属性,甚至对一些原生方法的赋值会导致原生方法的覆盖。比如下面对变量parseInt进行赋值,将覆盖原生parseInt方法。

var parseInt = function(number) {
  return "hello"
}
parseInt(123) // "hello"
window.parseInt(123) // "hello"

而通过关键字let在全局环境中进行变量声明时,新的变量将不会成为全局对象的一个属性,因此也就不会覆盖window对象上面的一些原生方法了。如下面的例子:

let parseInt = function(number) {
  return "hello"
}
parseInt(123) // "hello"
window.parseInt(123) // 123

在上面的例子中,我们看到let生命的函数parsetInt并没有覆盖window对象上面的parseInt方法,因此我们通过调用window.parseInt方法时,返回结果123。

在多次声明同一变量时处理不同

在ES2015之前,可以通过var多次声明同一个变量而不会报错。下面的代码是不会报错的,但是是不推荐的。

var a = "xiaoming"
var a = "huangxiaoming"

其实这一特性不利于我们找出程序中的问题,虽然有一些代码检测工具,比如ESLint能够检测到对同一个变量进行多次声明赋值,能够大大减少我们程序出错的可能性,但毕竟不是原生支持的。不用担心,ES2015来了,如果一个变量已经被声明,不论是通过var还是let或者const,该变量再次通过let声明时都会语法报错(SyntaxError)。如下代码:

var a = 345
let a = 123 // Uncaught SyntaxError: Identifier "a" has already been declared
最好的总是放在最后:const

通过const生命的变量将会创建一个对该值的一个只读引用,也就是说,通过const声明的原始数据类型(number、string、boolean等),声明后就不能够再改变了。通过const声明的对象,也不能改变对对象的引用,也就是说不能够再将另外一个对象赋值给该const声明的变量,但是,const声明的变量并不表示该对象就是不可变的,依然可以改变对象的属性值,只是该变量不能再被赋值了。

const MY_FAV = 7
MY_FAY = 20 // 重复赋值将会报错(Uncaught TypeError: Assignment to constant variable)
const foo = {bar: "zar"}
foo.bar = "hello world" // 改变对象的属性并不会报错

通过const生命的对象并不是不可变的。但是在很多场景下,比如在函数式编程中,我们希望声明的变量是不可变的,不论其是原始数据类型还是引用数据类型。显然现有的变量声明不能够满足我们的需求,如下是一种声明不可变对象的一种实现:

const deepFreeze = function(obj) {
    Object.freeze(obj)
    for (const key in obj) {
        if (typeof obj[key] === "object") deepFreeze(obj[key])
    }
    return obj
}
const foo = deepFreeze({
  a: {b: "bar"}
})
foo.a.b = "zar"
console.log(foo.a.b) // bar
最佳实践

在ECMAScript 2015成为最新标准之前,很多人都认为let是解决本文开始罗列的一系列问题的最佳方案,对于很多JavaScript开发者而言,他们认为一开始var就应该像现在let一样,现在let出来了,我们只需要根据现有的语法把以前代码中的var换成let就好了。然后使用const声明那些我们永远不会修改的值。

但是,当很多开发者开始将自己的项目迁移到ECMAScript2015后,他们发现,最佳实践应该是,尽可能的使用const,在const不能够满足需求的时候才使用let,永远不要使用var。为什么要尽可能的使用const呢?在JavaScript中,很多bug都是因为无意的改变了某值或者对象而导致的,通过尽可能使用const,或者上面的deepFreeze能够很好地规避这些bug的出现,而我的建议是:如果你喜欢函数式编程,永远不改变已经声明的对象,而是生成一个新的对象,那么对于你来说,const就完全够用了。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/90856.html

相关文章

  • ES6系列】变量与块级作用

    摘要:不允许在相同作用域内,重复声明同一个变量。如但是在中则不再必要了,我们可以通过块级作用域就能够实现本次主要针对中的变量和块级作用域进行了梳理学习,并且通过与的实现方式进行了对比,从而看出其变化以及快捷与便利。 ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可...

    PascalXie 评论0 收藏0
  • ES6 走马观花(ECMAScript2015 新特性)

    摘要:字面上是生成器的意思,在里是迭代器生成器,用于生成一个迭代器对象。当执行的时候,并不执行函数体,而是返回一个迭代器。迭代器具有方法,每次调用方法,函数就执行到语句的地方。也有观点极力反对,认为隐藏了本身原型链的语言特性,使其更难理解。 本文为 ES6 系列的第一篇。旨在给新同学一些指引,带大家走近 ES6 新特性。简要介绍: 什么是 ES6 它有哪些明星特性 它可以运行在哪些环境 ...

    wangzy2019 评论0 收藏0
  • ES6学习手稿之基本类型扩展

    摘要:它是一个通用标准,奠定了的基本语法。年月发布了的第一个版本,正式名称就是标准简称。结语的基本扩展还有一些没有在这里详细介绍。 前言 ES6标准以及颁布两年了,但是,好像还没有完全走进我们的日常开发。这篇文章从ES6的基本类型扩展入手,逐步展开对ES6的介绍。 ECMAScript和JavaScript JavaScript是由Netscape创造的,该公司1996年11月将JavaSc...

    tommego 评论0 收藏0
  • JavaScript从初级往高级走系列————ES6

    摘要:采用二八定律,主要涉及常用且重要的部分。对象是当前模块的导出对象,用于导出模块公有方法和属性。箭头函数函数箭头函数把去掉,在与之间加上当我们使用箭头函数时,函数体内的对象,就是定义时所在的对象,而不是使用时所在的对象。 ES6 原文博客地址:https://finget.github.io/2018/05/10/javascript-es6/ 现在基本上开发中都在使用ES6,浏览器环境...

    孙淑建 评论0 收藏0
  • ES2015入门系列2-let和const

    摘要:新增了两个变量修饰关键字它们都是块级别的,那什么是块简单的来说,块就是一组花括号中间的部分。全局变量使用基本上可以不用了 ES2015 新增了两个变量修饰关键字: let const 它们都是块级别的,那什么是块?简单的来说,块就是一组花括号中间的部分。 Var 为了理解let我们先从var说起,如下代码: function checkStatus(status) { if (...

    godiscoder 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<