资讯专栏INFORMATION COLUMN

揭秘变量提升

lanffy / 2411人阅读

摘要:想阅读更多优质文章请猛戳博客一年百来篇优质文章等着你引用规范作者一条最近的推特变量提升是一个陈旧且令人困惑的术语。变量提升部分提前激活是在和之前声明变量的一种较老的方法。

为了保证可读性,本文采用意译而非直译。

想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你!

引用 ES6 规范作者 Allen Wirfs-Brock一条最近的推特:

变量提升是一个陈旧且令人困惑的术语。甚至在 ES6
之前:变量提升的意思究竟是“提升至当前作用域顶部”还是“从嵌套的代码块中提升到最近的函数或脚本作用域中”?还是两者都有?

受 Allen 启发,本文提出了一种不同的方法来描述变量声明。

1. 声明:作用域与激活

可以将声明分为两个方面:

作用域:在哪里可以看到声明的变量? 这是一个静态特征。

激活:我什么时候可以访问变量? 这是一个动态特征:有些变量只要我们进入其作用域,就可以访问。 有的,我们必须等到执行到它们的声明。

下表总结了不同声明的方式如何处理上述两个方面。

“Duplicates”描述是否可以在同一作用域内声明两次。

“Global prop.”表示一个在 script 中的声明,当全局作用域中被执行时,是否会向全局对象添加属性。

TDZ 表示暂时性死区(稍后解释)。 函数声明在严格模式下是块作用域的(例如在模块内部),但在非严格模式下是函数作用域。

2. const 和 let :暂时性死区

对于JavaScript,TC39 需要决定如果在声明之前访问其直接作用域中的常量会发生什么:

{
  console.log(x); // 这里会发生什么?
  const x;
}

主要有两种种情况:

打印 undefined

报错

第一种不会出现,因为 x 是一个常量,如果打印 undefined,在声明前和声明后它将拥有不同的值,x 就不是常量了。

let 和 const 都会出现第二种情况,就是会报错。进入变量作用域与执行声明之间的这段时间被称为该变量的 临时死区(TDZ)

在临时死区中,变量被认为是未初始化的(就像它有一个特殊的值一样)。

如果访问未初始化的变量,将得到ReferenceError 错误。

一旦执行到变量声明,该变量将被设置为初始化器的值(通过赋值符号指定),如果没有初始化,则为undefined

以下代码说明了临时死区:

if (true) { // 进入 `tmp` 的作用域,TDZ 开始
  // `tmp` 未被初始化:
  assert.throws(() => (tmp = "abc"), ReferenceError);
  assert.throws(() => console.log(tmp), ReferenceError);

  let tmp;  // TDZ 结束
  assert.equal(tmp, undefined);
}

下一个例子表明临时死区只是 暂时的 (与时间有关):

if (true) { // 进入 `myVar` 作用域,TDZ 开始
  const func = () => {
    console.log(myVar); // 稍后执行
  };

  // 我们在 TDZ 中:
  // 访问 `myVar` 造成 `ReferenceError`

  let myVar = 3; // TDZ 结束
  func(); // OK,在 TDZ 外调用
}

即使 func() 位于myVar声明之前使用 myVar 变量,但我们也可以调用func(),前提是必须等到myVar的临时死区结束。

函数声明与提前激活

函数声明总是在进入它的作用域时执行,不管它位于作用域的什么位置。这使得能够在函数foo()声明之前调用它:

assert.equal(foo(), 123); // ok,相等
function foo() { return 123; }

提前激活 foo()意味着楼上的代码等价于

function foo() { return 123; }
assert.equal(foo(), 123);

如果用 constlet 声明一个函数,它就不会被提前激活:在下面的例子中,你只能在 bar() 声明后调用它。

assert.throws(
  () => bar(), // 声明前
  ReferenceError);

const bar = () => { return 123; };

assert.equal(bar(), 123); // 声明后
在没有提前激活的情况下提前调用

即使函数g()没有提前激活,也可以被前面的函数 f()(在同一作用域内)调用 - 只要遵守以下规则:f() 必须在声明 g() 之后调用

const f = () => g();
const g = () => 123;

// g() 声明后调用 f():
assert.equal(f(), 123);

模块中的函数通常在模块执行完后调用。 因此,在模块中,很少需要担心函数的顺序。

最后,注意提前激活是怎样自动执行以维持上述规则的:当进入一个作用域时,在任何函数被调用前,所有的函数声明都会被先执行。

提前激活的一个陷阱

如果依赖于提前激活机制,在函数声明之前调用函数,那么需要注意的是它不会访问未提前激活的变量。如下:

funcDecl();

const MY_STR = "abc";
function funcDecl() {
   console.log(MY_STR)
}

上述会报错:

如果你在 MY_STR 声明之后调用 funcDecl() 就不会有问题。

提前激活的利弊

我们已经看到提前激活有一个陷阱,你可以在不使用它的情况下获得大部分好处。因此,最好避免提前激活。但我对此说法并非十分认同,如前所述,我经常使用函数声明,因为我喜欢它们的语法。

类声明不会提前激活

类声明不会提前激活:

assert.throws(
  () => new MyClass(),
  ReferenceError);

class MyClass {}

assert.equal(new MyClass() instanceof MyClass, true);

这是为什么? 考虑以下类声明:

class MyClass extends Object {}

extends是可选的,它的操作数是一个表达式。 因此,您可以这样做:

const identity = x => x;
class MyClass extends identity(Object) {}

计算这样的表达式必须在它被引用的地方完成,其它行为都会使人困惑。这解释了为什么类声明不提前激活。

var :变量提升(部分提前激活)

var是在constlet之前声明变量的一种较老的方法。考虑下面的var声明。

var x = 123;

这个声明包含两个部分:

声明var x:与大多数其他声明一样,var声明变量的作用域是最内层的包围函数,而不是最内层的包围块。这样的变量在其作用域的开始时就已处于活动状态,并使用undefined初始化。

赋值 x = 123 :赋值总是在适当位置执行。

以下代码演示了 var

function f() {
  // 部分提前激活:
  assert.equal(x, undefined);
  if (true) {
    var x = 123;
    // 赋值已经执行
    assert.equal(x, 123);
  }
  // 作用域为函数作用域,非块级作用域。
  assert.equal(x, 123);
}
交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq44924588...

我是小智,公众号「大迁世界」作者,对前端技术保持学习爱好者。我会经常分享自己所学所看的干货,在进阶的路上,共勉!

关注公众号,后台回复福利,即可看到福利,你懂的。

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

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

相关文章

  • 揭秘变量提升

    摘要:想阅读更多优质文章请猛戳博客一年百来篇优质文章等着你引用规范作者一条最近的推特变量提升是一个陈旧且令人困惑的术语。变量提升部分提前激活是在和之前声明变量的一种较老的方法。 为了保证可读性,本文采用意译而非直译。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 引用 ES6 规范作者 Allen Wirfs-Brock一条最近的推特: 变量提升是一个陈旧且令人困惑的...

    Harpsichord1207 评论0 收藏0
  • 揭秘】云计算技术的现状及特点

    摘要:云计算有哪些技术特点很多技术宅的专家们曾经总结过云计算技术的一些特点,下面我们就来概述一下。目前,云计算技术在安全问题上已经有了长足的进步,通过对数据传输数据存储数据审计三个环节采取相应的安全措施,从而保障整个云平台的安全。  云计算当前处于何种现状?  我们在谈任何一种新技术的时候,用户往往更关注技术所在领域的应用程度等问题,对于云计算来说,近些年在科研领域的应用已经取得了突飞猛进的进展,...

    2i18ns 评论0 收藏0
  • QuarkChain技术揭秘 第一话:分片遇上区块链

    摘要:区块链水平扩容的基本思想是将单根区块链的状态划分为多条区块链状态。通过增加网络中片的数量,整个区块链网络的吞吐量将会线性增加。的宗旨是通过以分片为代表的水平扩容技术,建立一个人人可用的区块链底层公链。 目前,公链极低的交易处理能力(TPS)为人们便捷的使用区块链带来很大的麻烦。例如:比特币网络只支持6到7个TPS,而以太坊目前只能处理大约15 TPS,而中心化支付系统的代表:支付宝,在...

    Coly 评论0 收藏0
  • php数组原理遍历原理揭秘

    摘要:数组原理遍历原理揭秘数组原理遍历原理揭秘可见,数组其实已经改变了,但是遍历出来的并没有增加的哪一项。此时,我们也可以输出一下当前指针位置数组原理遍历原理揭秘数组原理遍历原理揭秘数组指针停留在了位置上。 php中的中的数组跟js里面数组是不大一样的。php中数组的下标可以整数也可以是字符串,而且数组中元素的顺序不是由下标决定的,而是由添加元素的顺序。数组基础 $arr1 = array(...

    wanghui 评论0 收藏0

发表评论

0条评论

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