felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

JavaScript Hoisting(提升) #143

Open felix-cao opened 5 years ago

felix-cao commented 5 years ago

英文 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 commented 3 years ago

考验基础的面试题

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 commented 3 years ago

全局整体分析:

felix-cao commented 3 years ago

code1

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

felix-cao commented 3 years ago

code2

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

felix-cao commented 3 years ago

code3

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

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

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 commented 3 years ago

code4

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

code5

运行结果: 2