重学前端: JavaScript中的提升Hoisting机制

713 阅读6分钟

JS中除了变量提升、函数提升外,作用域链延长也是JS提升机制(Hoisting)的一部分。

一.变量提升、函数提升

变量提升的意义:

JS拿到一段代码或一个函数的时候,会有两步主要操作即解析与执行,在解析阶段,JS会做语法检查和预编译。函数代码有错误时,函数执行前会抛出SyntaxError

  • 声明提升可以提高性能,解析和预编译过程中的声明提升可以提高性能,让函数可以在执行时预先为变量分配栈空间
  • 容错性更好,在发布之后很长时间内都没有为程序员提供编译器、调试器、语法检查器等工具。

blog.csdn.net/weixin_4346…

防止变量提升:

1.在严格模式中,为未声明的标识符赋值将会抛引用错误,因此可以防止意外的全局变量属性的创造。 2.ES6针对这个进行了改变,加入了let/const 后,变量Hoisting就不存在了。

这里研究的目的是更全面的理解js的这些机制。

1.用var关键字声明和不用关键字声明

区别

  • 1.无var在解析过程中提升,提前引用会抛“is not defined”
  • 2.有var声明的变量仅提升声明过程的前两步,提前引用会抛“undefined”

变量声明过程

// var 声明的「创建、初始化和赋值」过程
if (true) {
    // 只是【创建(就不再是is not defined)、初始化为undefined】两步提升了,但是并没有赋值过程,所以是下面的输出结果
    console.log(x, y) // undefined,undefined
    var x = 1
    var y = 2
}
// case2
if (true) {
    // 解析前引用必然是没有声明创建
    console.log(x, y) // Uncaught ReferenceError: y is not defined
    var x = 1
    y = 2
}

代码块作用域是两个不同阶段的概念, 描述代码时使用{}括起来的代码被称为代码块,js执行时作用域是js的一个概念,js执行时限定其可用性的范围即作用域,作用域的使用提高了程序逻辑的局部性,增强程序的可靠性,减少名字冲突。

baike.baidu.com/item/%E4%BD…

变量提升和作用域、作用域链的关系

function test() {
// 不用var关键字声明a,则a会在test作用域中查找,这里查不到则会在全局中创建
a = 1;
var b; // 如果是这样写 this.b = '';就是test的属性,b只是test函数的一个变量 
console.log(a); // 1
}
test();
console.log(a); // 使用全局中创建的变量

作用域的作用

  • var声明会提升到作用域顶端,局部的变量不会变为全局的属性(《javascript高级程序设计》时(page194)详细说明全局变量和全局属性的区别,全局属性是可以delete掉的);
  • 不用var声明会在当前作用域链中按顺序解析a,如果在当前作用域链没有发现声明a,则会在全局中创建属性并赋值; 如果在当前作用域链中发现声明则会执行赋值。
  • 作用域顶端是一个集合,这样就解决了顺序问题。

作用域链的作用

  • 保证执行环境有序的访问到所有变量和函数。

2.函数提升

声明方式

//函数声明式
function bar () {}
//函数字面量式 
var foo = function () {}

函数提升机制

// 代码顺序
console.log(bar);
function bar () {
console.log(1);
// 执行顺序是这样的
function bar () {
  console.log(1);
}
console.log(bar);
// 函数提升是整个代码块提升到它所在的作用域的最开始执行
  • 函数声明式,js的机制会将函数提升到作用域顶端
  • 采用字面量式声明函数就和函数提升没有关系了,这里的函数仅是变量的值。

测试题

测试题1

var x = 1, y = 2; // 全局变量
// z也是全局变量 
var z = function () {
    var x = 2;
    return {
        x: x, // 值类型,值为2就不会变了
        y: function (a, b) {
            // x为 “var x = 2;” a.y(x, y);执行完后 x就变成3
            x = a + b;
        },
        z: function () {
            return x; // 这里的x也是 “var x = 2;” 
        }
    }
};
// 最外层没有能形成作用域的代码块,到此为止xyz都是window的全局变量
// a的声明方式会变成全局属性,值为z函数执行后返回结果
a = z();
a.y(x, y); // 执行后x为3 
console.log(a.z(), a.x, x); // 3,2,1

测试题2

foo(); //高版本: Uncaught TypeError: foo is not a function
var a = true; // a这个变量提升没问题
if(a){ // 有代码块逻辑,下面是先执行foo(); 再执行这个判断
    function foo () { console.log(1); }
}else{
    function foo () { console.log(2); }
}
// 所以foo执行前都没有声明foo
// 低版本:2 ,这个就没必要研究了,不合理的存在

这意味着无论作用域中的var声明出现在什么地方,都将在代码本身被执行前首先进行处理,可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为提升。

测试题3

for(var i=0;i<5;i++){
    var a=1;
   console.log(i);
}
console.log(a, i); //在for循环外也是可以访问到a和i的

块级作用域

示例

if(true) {var a = '666'} console.log(a); // 666
if(true) {let a = '666'} console.log(a); // Uncaught ReferenceError: a is not defined
  • ES6之前JS是没有块级作用域的。这意味着在块语句中定义的变量,实际是在函数中创建的,而不是语句中。 函数可以模仿块级作用域。
  • ES6通过新增命令let和const来实现。
  • 可以使用自执行函数来模拟块级作用域

二.延长作用域链

  • 执行环境也叫做上下文 context ,在全局中也就是 window对象,在函数中执行环境就是函数体

  • 执行环境的类型只有两种,全局和局部(函数,块级作用域)。但是有些语句可以在作用域链的前端临时增加一个变量对象,该变量对象会在代码执行后被移除。

两种延长作用域链的方法

  • try...catch...
  • with

1.try - catch 语句的catch块会创建一个新的变量对象,包含的是被抛出的错误对象的声明。 编译器遇到catch语句块(catch可以看做一个处理异常错误的函数)会提升到作用域链的前端。

2.with 语句会将指定的对象的属性添加到作用域链中。

作用说明

1.延长作用域链的意义是对作用域链概念的一种特殊说明。是提升机制完整阐述的重要部分。

2.延长作用域链在实际开发中也很有用处。

3.注意不要频繁的使用with, 会造成很多无用的变量释放,浏览器需要频繁的解析(语法检查和预编译)影响性能。

// 释放多个对象的属性需要以此嵌套
with(location){  with(screen){ console.log(href, availWidth)}}

4.虽然很多人不提倡使用with, 个人觉得大可不必排斥它,如果with能让你感觉到爽就可以使用。

vue很重要的render函数就是使用with实现的,为什么要搞成字符串呢? 因为严格模式下不允许使用with。

    function generate (
    ast,
    options
  ) {
    var state = new CodegenState(options);
    var code = ast ? genElement(ast, state) : '_c("div")';
    return {
      render: ("with(this){return " + code + "}"),
      staticRenderFns: state.staticRenderFns
    }
  }

测试题

测试题1

function buildUrl() {
    var qs = "?debug=true";
    var href = 'charlesyu01'
    with (location){ // 你可以不用,但不能不知道; with只在声明中的代码块中有效
      var url = href // 这会让你的代码变得简洁很多;
    }
    console.log(url) // url本提升了; url的值既不是undefined也不是charlesyu01,而是location.href值
  }
  buildUrl();
  • 用with声明的代码块,上面的参数对象释放的属性仅在with声明中有效
  • 和块级作用域防止变量提升不同,with是为了更简洁的访问外部数据。
  • with每次只能释放一个对象的属性

测试题2

为何try...catch可以在catch中捕获到异常信息呢,js是怎么实现的呢

try..catch只是处理语句,异常是解析器和执行中通过throw抛出的一个exception,然后交由try... 语句处理
// js的常见异常类型
EvalErrorRangeErrorReferenceErrorSyntaxErrorTypeErrorURIError
// 参考官网https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
// MZ基金会可以说是JS语言的官网,任何与js相关的定义和描述都可以在这里找到
// 而W3C是一个民间组织,没有约束性,因此只提供建议;
// chrome特有的一些API或实现方法 也可以去chrome官网查阅资料