你不知道的JS系列 ( 12 ) - 声明提升

我们直觉上会认为 JavaScript 代码在执行时是由上到下一行一行执行的。但实际这并不完全正确

a = 2;
var a;
console.log(a);

这里可能会认为是 undefined,因为 var a 声明在 a = 2 之后。实际输出了 2。

console.log(a);
var a = 2;

鉴于上面的代码可能会是 2,还有人认为可能会报异常 ReferenceError,不幸这两种猜测都不对,输出来的是 undefined

要搞明白这个,要明白编译的原理,在编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。这也是词法作用域的核心内容

正确的思路是,包括变量和函数在内的所有声明都会在任何代码被执行前首先被处理。

当你看到 var a = 2; 时,可能会认为这是一个声明。但 JavaScript 实际上会将其看成两个声明:var a 和 a = 2;第一个定义声明是在编译阶段进行的,第二个赋值声明会被留在原地等待执行。所以第一段代码会以如下形式进行处理

var a;
a = 2;
console.log(a); //2

第二段代码实际按一下流程处理

var a;
console.log(a); // undefined
a = 2;

这个过程就好像变量和函数声明从它们在代码中出现的位置被“移动”到了最上面。这个过程就叫做提升。每个作用域都会进行提升操作,函数自身也会对内容 var a 进行提升(显然并不是提升到整个程序的最上方)

foo(); // TypeError
bar(); // ReferenceError
var foo = function bar(){
  // ...
}

可以看到函数声明被提升,但是函数表达式却不会被提升。这段程序的 foo 被提升并分配到所在作用域,因此不会导致 ReferenceError,而是抛出了 TypeError: foo is not a function。bar 抛出了 ReferenceErro。上段代码实际上会被理解为

var foo;

foo();
bar();

foo = function() {
  var bar = ...self...
  //...
}

变量和函数都会被提升。但是函数会首先被提升,然后才是变量

foo(); // 1
var foo;
function foo() {
  console.log(1);
}
foo = function(){
  console.log(2)
}

这个代码会被引擎理解成如下形式

function foo() {
  console.log(1)
}
foo(); // 1
foo = function() {
  console.log(2)
}

尽管重复的声明会被忽略掉,但是后面的函数声明还是可以覆盖前面的

foo();
var a = true;
if(a){
  function foo() {
    console.log(‘a‘)
  }
}else{
  function foo(){
    console.log(‘b‘)
  }
}

这里 foo 输出了 b ,不是我们想要的结果,因为声明被覆盖掉了,所以需要注意这种行为并不可靠,尽可能避免在块内部声明函数

原文地址:https://www.cnblogs.com/wzndkj/p/12348491.html

时间: 2024-10-06 09:47:29

你不知道的JS系列 ( 12 ) - 声明提升的相关文章

JS中的声明提升问题

我们习惯将 var a = 2; 看作一个声明,而实际上JavaScript引擎并不这么认为.他将 var a 和 a = 2 当作两个单独的声明,第一个是编译阶段的任务,第二个则是执行阶段的任务. --<你不知道的Js> 变量提升 变量提升的概念已经为大家所熟知,简单来讲就是在代码执行前编译器会将变量的声明提升至其所在作用域(不是全局作用域)的顶端.但在这过程中还有一些细节需要注意.比如赋值与声明提升的先后关系.先看一个例子. 由于存在变量声明提升,对 a 的声明已经提升至最前,这里打印变量

你不知道的JS系列 ( 5 ) - 词法作用域

作用域分为两种,一种是词法作用域,一种是动态作用域,我们先看第一种,词法作用域 词法作用域就是定义在词法阶段的作用域(编译器的第一个工作阶段叫做词法化,词法化的过程会对源代码中的字符进行检查).换句话说,词法作用域是由你在写代码时将变量和块作用域写在哪里来决定的.因此词法分析器处理代码时会保持作用域不变 function foo(a) { var b = a * 2; function bar(c){ console.log( a, b, c ); } bar( b * 3 ); } foo(

你不知道的JS系列(2)- 引擎怎么查找变量

对代码进行处理的三个角色 引擎:从头到尾负责整个 JavaScript 程序的编译和执行过程 编译器:负责语法分析及代码生成等 作用域:负责收集并维护所有变量的查询 var a = 2; 编译器首先会将这段程序分解成词法单元,然后将词法单元流解析成一个树结构.然后将树结构转换成可执行代码,也就是计算机懂的指令.为一个变量分配内存,将其命名为 a,然后将值 2 保存进这个变量.这符合编译原理 然而并不完全正确 事实上编译器会进行如下处理 1.在词法分析中,遇到 var a,编译器会询问当前作用域是

你不知道的JS系列 ( 7 ) - 欺骗词法作用域

如果词法作用域完全由写代码期间函数所声明的位置来定义,怎样才能在运行时来“修改”词法作用域呢?有些人喜欢特殊的办法来解决遇到的问题.我们规定词法作用域是代码写在哪里决定的,一旦决定了无法更改,因为一些问题,我们不得不更改作用域,尽管这是不被推荐的,那是什么办法,JavaScript 中有两种机制来实现这个目的 eval function foo(str, a) { eval(str); console.log(a, b) } var b = 2; foo("var b = 3", 1)

你不知道的JS系列 ( 13 ) - 什么是闭包

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行 function foo(){ var a = 2; function bar() { console.log(a); } return bar } var baz = foo(); baz(); // 2 —— 朋友,这就是闭包的效果 在 foo() 执行后,通常会期待 foo() 的整个内部作用域都被销毁,因为我们知道引擎有垃圾回收用来释放不再使用的内存空间. 而闭包的“神奇”之处正是可以阻止这件事情的

你不知道的JS系列 ( 33 ) - 对象复制

JS 初学者最常见的问题之一就是如何复制一个对象.看起来应该有一个内置的 copy() 方法,实际上比想象中的更复杂,我们无法选择一个默认的复制算法 function anotherFunction(){/** ... */}; var anotherObject = { c: true }; var anotherArray = []; var myObject = { a: 2, b: anotherObject, // 引用,不是复本! c: anotherArray, // 另一个引用

你不知道的JS系列 ( 19 ) - this 调用位置

我们排除了一些对于 this 对错误理解并且明白了每个函数的 this 是在调用时被绑定的,完全取决于函数的调用位置.寻找调用位置就是寻找“函数被调用的位置”,但是做起来并没有这么简单,因为某些编程模式可能会隐藏真正的调用位置 最重要的是要分析调用栈,就是为了到达当前执行位置所调用的所有函数 function baz(){ // 调用位置是全局作用域,调用栈 baz console.log('baz'); bar(); // bar 的调用位置 } function bar(){ // 调用位置

你不知道的JS系列 ( 22 ) - this new 绑定

在传统的面向类的语言中,“构造函数“是类中的一些特殊方法,使用 new 初始化类时会调用类中的构造函数.通常的形式是这样的 something = new MyClass(..); 然而 JavaScript 中 new 的机制实际上和面向类的语言不同.它们只是被 new 操作符调用的普通函数而已.实际上并不存在所谓的“构造函数”,只有对于函数的“构造调用” function foo(a) { this.a = a; } var bar = new foo(2); console.log(bar

你不知道的JS系列 ( 23 ) - this 绑定优先级

我们首先来看下隐式绑定和显示绑定哪个优先级更高 function foo(){ console.log(this.a) } var obj1 = { a: 2, foo: foo } var obj2 = { a: 3, foo: foo } obj1.foo(); // 2 obj2.foo(); // 3 obj1.foo.call(obj2); // 3 obj2.foo.call(obj1); // 2 这个例子可以看到,显示绑定优先级比隐式绑定更高. 现在我们需要搞清楚 new 绑定和