Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript Hoisting(提升) #143

Open
felix-cao opened this issue Jan 25, 2019 · 6 comments
Open

JavaScript Hoisting(提升) #143

felix-cao opened this issue Jan 25, 2019 · 6 comments

Comments

@felix-cao
Copy link
Owner

felix-cao commented Jan 25, 2019

英文 Hoisting ['hɔɪstɪŋ],直译过来是提升的意思,也交预处理。 在 ES6 之前的 JavaScript 开发中,我们经常遇到这种 Hoisting 的场景,但这种设计是很低劣的,被很多语言专家认为是 JavaScript 最大的设计败笔之一,本文主要来聊一聊这种 Hoisting.

造成这种 Hoisting 的原因,大概是JavaScript 的作用域规则的确定阶段造成的,我们在《JavaScript 执行上下文和执行上下文栈(Execution Context Stack简称ECS)》,一文中提到 JavaScript 代码的整个执行过程,分为两个阶段:

  • 代码编译阶段。编译阶段由编译器完成,将代码翻译成可执行代码,作用域规则在这个阶段确定
  • 代码执行阶段。执行阶段由引擎完成,主要任务是执行可执行代码,执行上下文在这个阶段创建。

那么在编辑阶段确定了全局变量和所有的函数的作用域规则,具体的操作是将所有声明(Declaration)的变量和声明的函数提升到对应作用域的顶部,即:

Hoisting in JavaScript means that variable and function declarations are moved to the top of their containing scope.

很多网络资料把这叫做 变量提升,其实只说对了一半,严谨的叫法应该是 声明提升

一、变量声明提升(variable declaration hoisting)

console.log( blog ); // undefined
var blog = 'Felix';

对于上面的例子,先打印 blog 变量,再使用关键字 var 去声明变量 blog,在浏览器的 Console 下运行,我们发现并没有报错,而是输出 undefined.

那为什么会这样呢?因为浏览器在编译阶段做了 Hoisting 的动作处理,Hoisting 后的代码是这样的:

var blog;
console.log( blog ); // undefined
blog = 'Felix';

二、函数声明提升(function declaration hoisting)

函数声明提升指的是 整个函数体的提升
JavaScript 语言中,创建函数有两种常见的方式: 函数声明和函数表达式,在这个两种方式中,只有函数声明具有 整体提升特性

console.log(show); // 打印出函数
show();
function show() {
  console.log('Felix');
}

上面的代码,console.log(show); 打印出整个函数,证明函数声明提升指的是 整个函数体的提升,函数show() 的执行晚于其定义。

console.log(showBlog); // undefined
console.log(showBlog()); // Uncaught TypeError: showBlog is not a function
var showBlog = function() {
  console.log('Felix Blog');
}

上面的代码是函数表达式,不具有 整体提升特性

三、函数声明提升优于变量声明提升

相比与变量声明提升,函数声明提升会优先进行, 因为JavaScript中的函数是一等公民,函数声明的优先级最高,会被提升至当前作用域最顶端。

console.log(show); // 打印出函数体
show(); // Felix
function show() {
  console.log('Felix');
}

var show = 'felix cao'

上面的代码,show 函数和 变量 show, 名称相同,那么他们两个都应该提升了,按照变量提升去理解第一行应该打印出 undefined, 但是这里却打印出了show函数体,由此证明函数声明提升优于变量声明提升,所以上面的代码经过编译阶段后是这样的:

function show() {
  console.log('Felix');
}
console.log(show); // 打印出函数体
show(); // Felix

var show = 'felix cao'

思考一下,下面的代码输出情况

function show() {
  console.log('Felix');
}
console.log(show);
var show = 'felix cao';
console.log(show);

四、Hoisting 是 JavaScript 语言最大的设计败笔之一

通过前面几段代码的解读,即使是 JavaScript 高手也有种读绕口令的困惑,JavaScript 的这种提升行为(behavior)给程序开发者造成了很大的 confusion(困惑)和很烂的开发体验,被很多 JavaScript 专家认为是 JavaScript 语言最大的设计败笔之一。

为了改善这种 Hoisting 现象,在很多公司或组织内部的 JavaScript 规范中(英文叫 JavaScript Standard Style),规定变量必须先声明后使用。

但规范并不能解决根本问题,幸运的是,这种 HoistingES6 中得到巨大的改善。在 ES6 中我们不再提倡使用 var 去声明变量,而是提倡 letconst 去声明变量,ES6通过暂时性死区和letconst的使用,防止变量在声明前就去使用从而导致意料之外的行为。

无论是流行的 JavaScript 规范还是 ES6 新增的 letconst ,都是为了解决这种设计败笔,业界一致认为:变量应该是先声明后使用, 这样的设计是为了让大家养成良好的编程习惯,提高程序的可读性和可维护性。

Reference

@felix-cao
Copy link
Owner Author

felix-cao commented Nov 24, 2020

考验基础的面试题

function Foo () {
   getName = function () { alert(1) }
   return this
}
Foo.getName = function () { alert(2) }
Foo.prototype.getName = function () { alert(3) }
var getName = function () { alert(4) }
function getName () { alert(5) }

Foo.getName(); // code1
getName(); // code2
Foo().getName(); // code3
getName(); // code4
new Foo.getName(); // code5
new Foo().getName(); // code6
new new Foo().getName(); // code7

输出结果是啥?

@felix-cao
Copy link
Owner Author

felix-cao commented Nov 24, 2020

全局整体分析:

  • 函数:Foo的函数
  • 对象:函数 Foo 也是对象,把 Foo 当成一个对象看待,在Foo 这个对象上挂一个叫getName的静态属性存储了一个匿名函数
  • 对象的原型:为Foo的原型对象新创建了一个叫getName的匿名函数
  • 函数变量表达式:通过函数变量表达式创建了一个getName的函数
  • 函数声明:声明一个叫getName函数

@felix-cao
Copy link
Owner Author

felix-cao commented Nov 25, 2020

code1

运行结果是 2,执行 Foo 对象上的 getName 函数

@felix-cao
Copy link
Owner Author

felix-cao commented Nov 25, 2020

code2

运行结果: 4,
主要知识点

  • 变量的 Hoisting , 即变量提升,比较全面的说法应该是 声明提升
  • 函数的声明提升要优与变量的声明提升
    上面的代码变量提升后的顺序为:
function getName () { alert(5) }
var getName = undefined
getName = function () { alert(4) }

因此 code2 的执行结果为 4

@felix-cao
Copy link
Owner Author

felix-cao commented Nov 25, 2020

code3

运行结果: 1.
主要知识点:

  1. 函数内未使用 var 声明变量的,将自动挂载到顶层对象 window, 所以 Foo 内的 getName 如下
function Foo () {
   window.getName = function () { alert(1) }
   return this
}
  1. 顶层对象的属性与全局对象是等同

Foo() 执行后返回一个 this, 这里的 thiswindow 因此 Foo().getName(), 指的是 window.getName(), 即 getName(),

  1. 函数覆盖
    var getName = function () { alert(4) }function getName () { alert(5) } 覆盖, Foo().getName() 执行后会把 var getName = function () { alert(4) } 定义的 getName 函数表达式覆盖。

因此这里的结果是 1

@felix-cao
Copy link
Owner Author

felix-cao commented Nov 25, 2020

code4

运行结果: 1
code3Foo() 执行后, Foo() 里的 getName 函数会覆盖之前的函数, 因此 code4 处执行 getName() 为1

code5

运行结果: 2

  • 知识点, js 的运算符优先级: 点 运算符高于 new(无参数列表)
    使用 new 操作符 调用 Foo 对象上的 getName 构造器, 相当于 new (Foo.getName)();

code6

运行结果: 3

  • new Foo().getName() ,比较的是 new(有参数)、成员访问、执行函数() 的优先级,这几个是同级的,从左到右依次执行,就是 ((new Foo()).getName)(),最先执行的是 new Foo() 得到一个新的对象,而这个新的对象里没有 getName() 方法,所以就会去它的构造器的原型去找。分解为如下步骤:
const obj = new Foo();
obj.getName()

code7

运行结果: 3
与code6 一样的思路,分解为如下步骤:

new ((new Foo()).getName)();
const obj = new Foo();
new obj.getName();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant