小白成长日记-js提升篇-作用域和闭包

197 阅读7分钟

1.作用域

理解var a = 2的过程

  • 引擎------->负责整个JavaScript的编译及执行过程
  • 编译器----->语法分析与代码生成
  • 作用域----->确定当前代码对标识符(变量)的访问权限

过程

  1. var a 编译器询问作用域是否存在同名变量在作用域中,是忽略声明,否要求作用域在当前作用域创建一个新变量a
  2. 编译器为引擎生成运行时的代码--->处理a=2这个赋值操作,引擎询问作用域,是否存在a变量,是使用,否继续查找变量 引擎如果找到了a变量,则赋值,否则就抛出异常

image.png

LHS和RHS

含义是赋值操作的左侧还是右侧,但是最好理解为:

LHS(赋值操作的目标是谁)

RHS(谁是赋值操作的源头)

例如

console(a)->RHS 并没有赋值,在于找到a的值

a = 2 ->LHS 并不关心值是多少,而是为了找到一个目标

注意: 对于参数的传递,会有隐式的LHS查询

函数的声明不会有单另的线程将函数值分配给变量,所以说其为LHS不合适

作用域嵌套

LHS和RHS都会在当前作用域找不到时向上级作用域找,直到找到为止

异常

RHS在查询不到变量的时候,引擎会抛出ReferenceError,找到了但是进行不合理操作,例如给非函数类型进行函数调用,会抛出TypeError

ReferenceError和作用域判别失败相关,而TypeError代表作用域判别成功了,但是操作不合理

LHS在非严格模式下找不到目标变量会创建出一个变量来使用,而严格模式也会抛出ReferenceError

2.词法作用域

JavaScript采用的是词法作用域,还有一些编程语言在使用动态作用域

简单地说,词法作用域就是定义在词法阶段的作用域。换句话说,词法作用域是由你在写 代码时将变量和块作用域写在哪里来决定的,因此当词法分析器处理代码时会保持作用域 不变(大部分情况下是这样的)。

问题引入

遮蔽效应

在多层的嵌套作用域中可以定义同名的 标识符,这叫作“遮蔽效应”(内部的标识符“遮蔽”了外部的标识符)。抛开遮蔽效应, 作用域查找始终从运行时所处的最内部作用域开始,逐级向外或者说向上进行,直到遇见 第一个匹配的标识符为止。

window.a 通过这种技术可以访问那些被同名变量所遮蔽的全局变量。但非全局的变量 如果被遮蔽了,无论如何都无法被访问到。

欺骗词法

1.eval

eval接收一个字符串,让引擎认为代码原本就在这里

function foo(str, a) { 
    eval( str ); // 欺骗! 
    console.log( a, b );
}
var b = 2;
foo( "var b = 3;", 1 ); // 1, 3

注意: 严格模式下,eval有自己的作用域,声明无法修改其作用域

function foo(str) {
    "use strict";
    eval( str );
    console.log( a ); // ReferenceError: a is not defined
}
foo( "var a = 2" );

2.with

with 可以将一个没有或有多个属性的对象处理为一个完全隔离的词法作用域,因此这个对 象的属性也会被处理为定义在这个作用域中的词法标识符

白话: with会把对象作为一个单独的词法作用域,这个对象里面的属性会变为这个作用域的赋值语句

function foo(obj) { 
    with (obj) {
        a = 2;
    }
}
var o1 = {
    a: 3
};
var o2 = { 
    b: 3
};
foo( o1 );
console.log( o1.a ); // 2
foo( o2 );
console.log( o2.a ); // undefined
console.log( a ); // 2——不好,a 被泄漏到全局作用域上了!

解释: o1中的a被赋值为2,就输出2,o2中没有LHS找到a,由于是非严格模式,就会生成一个全局的a

性能

js引擎会在编译阶段进行很多优化,很多依据于代码的词法进行静态分析,预先确定变量和函数以快速找到标识符,两者都会创建新的作用域,在词法分析阶段无法明确知道eval的代码,和with的对象内容,可能会导致优化没有意义,会使性能变得更慢,谨慎使用

3.函数作用域和块作用域

这块读者自行查找,不做赘述

4.提升

我们都知道js会有变量提升这个概念,现在认真学习一波

为什么会有提升

引擎在解释js语句之前会先进行编译,编译阶段会找到所有的声明,并用适合的作用域联系起来,也就是词法作用域的内容,这就解释了我们的提升这个问题 当a = 2,var a的时候,var a会在编译阶段进行,赋值操作则会在原地等待。 注意: 函数声明会被提升,而函数表达式不会被提升

函数优先

函数声明和变量声明都会被提升。但是一个值得注意的细节(这个细节可以出现在有多个 “重复”声明的代码中)是函数会首先被提升,然后才是变量

foo(); // 1
var foo;
function foo() { 
    console.log( 1 );
}
foo = function() { 
    console.log( 2 );
};
会输出 1 而不是 2 !这个代码片段会被引擎理解为如下形式:
function foo() { 
    console.log( 1 );
}
foo(); // 1
foo = function() { 
    console.log( 2 );
};

5.闭包

我经常对于一些难懂的概念去选择逃避,但是总要去面对。。。

闭包定义

当函数可以记住并访问所在的词法作用域时,就产生了闭包,即使函数在当前词法作用域之外执行

我的理解:对于这个定义,首先是记住并访问,我们知道js引擎会释放不在使用的内存空间,而记住的效果就是不回收,以便我们即使不在使用这个函数,也可以访问到他的词法作用域。

function foo() { 
    var a = 2;
    function bar() { 
        console.log( a );
    }
    return bar; 
}
var baz = foo();
baz(); // 2 —— 朋友,这就是闭包的效果

在这段代码中foo已经执行完了,foo内部作用域应当被回收,但是我们通过函数返回的方法,用另外一个baz的引用去使用到了bar,去使用foo的内部作用域---->我们称之为bar拥有涵盖foo作用域的闭包,使得foo一直存活,以供bar在任何时间引用。

当然函数也可以值传递


function foo() { 
    var a = 2;
    function baz() { 
        console.log( a ); // 2
    }
    bar( baz ); 
}
function bar(fn) {
    fn(); // 妈妈快看呀,这就是闭包!
}

解释代码

function wait(message) {
     setTimeout( function timer() {
         console.log( message );
     }, 1000 ); 
}
wait( "Hello, closure!" );

timer函数作为参数传递给setTimeout,timer涵盖了wait作用域的闭包,保持了对message的引用 所以在wait执行完,wait的作用域不会消失,产生了闭包

循环中的闭包

看下面的代码

for (var i=1; i<=5; i++) { 
     setTimeout( function timer() {
         console.log( i );
     }, i*1000 );
}
//打印了5次6。

因为作用域的原理,实际上我们迭代了5次,使用的都是一个i,而回调函数在循环结束之后才会执行,所以我们输出了5次6

那我们用闭包解决吗

for (var i=1; i<=5; i++) { 
    (function() {
        setTimeout( 
            function timer() { 
                console.log( i );
             }, i*1000 );
     })();
}
//打印了5次6。

这显然是无意义的,因为我们虽然创建了闭包但是我们使用的是空的作用域

```js
for (var i=1; i<=5; i++) { 
    (function() {
        var j = i
        setTimeout( 
            function timer() { 
                console.log( j );
             }, i*1000 );//这里是参数,j和i都可,在循环时已经放入了参数
     })();
}
//打印了5次6。

我们在闭包中存入了自己的变量,就有正确的变量被我们访问

使用let

for (var i=1; i<=5; i++) {
    let j = i; // 是的,闭包的块作用域! 
        setTimeout( function timer() {
             console.log( j );
         }, j*1000 );
}
for (let i=1; i<=5; i++) { 
    setTimeout( function timer() {
        console.log( i );
    }, i*1000 );
}

let 声明了块作用域,也会封装我们需要的变量,也达到了效果。

for 循环头部的 let 声明还会有一 个特殊的行为。这个行为指出变量在循环过程中不止被声明一次,每次迭代都会声明。随 后的每个迭代都会使用上一个迭代结束时的值来初始化这个变量。

闭包的用途

1.模仿块级作用域

js并不会告知我们变量是否声明,所以容易造成命名冲突,而对于全局作用域中定义的变量,则会污染全局作用域,可以使用闭包来创建块级作用域

function X(num) {
    (function(){
        for(var i = 0; i < num.length; i++){
            num++
        }
    }).call() //声明一个函数立即调用以后,浏览器刷新页面会报错,可以用一个小括号把整段函数包起来。
    console.log(i)//undefined
}

函数X中,匿名函数形成了对x的闭包

这个闭包可以当函数X内部的活动变量,又能保证自己内部的变量在自执行后直接销毁。这种写法经常用在全局环境中,可以避免添加太多全局变量和全局函数,特别是多人合作开发的时候,可以减少因此产生的命名冲突等,避免污染全局环境。

2.存储变量

闭包的另一个特点是可以保存外部函数的变量,内部函数保留了对外部函数的活动变量的引用,所以变量不会被释放。

function S(){
    var a = 1
    return {
        function(){
            renturn a
        }
    }
}
var d = S() // 100

3.封装私有变量

函数内部的对象我们无法访问,可以通过闭包来访问,相当于将函数的对象进行了封装

var person = function(){
    //变量作用域为函数内部,外部无法访问
    var name = "default";
    return {
        getName : function(){
            return name;
        },
        setName : function(newName){
            name = newName;
        }
    }
}();
print(person.name);//直接访问,结果为undefined
print(person.getName()); // default
person.setName("abruzzi");
print(person.getName()); // abruzzi

参考文献