why-js-day1 作用域和闭包

作用域

编译

  1. 分词/词法分析(tokenizing/lexing)-----将代码块拆分为词法单元(token)
  2. 解析/语法分析(parsing)-----将词法单元转换为抽象语法树(AST)
  3. 代码生产-----将AST转换为可执行代码

关键角色

  • 引擎:从头到尾负责整个JavaScript的编译和执行过程
  • 编译器:负责词法分析以及代码生成
  • 作用域:负责收集并维护所有声明的标识符组成的一系列查询,并实施严格的规则,确定当前执行代码对标识符的访问权限

编译器在对 var a = 2 进行代码生成时会进行如下处理

  1. 编译器询问作用域是否有一个该名称的变量存在于同一作用域中,如果是,则忽略该声明,继续进行编译,否则会在当前作用域中声明一个新的变量,命名为a
  2. 然后编译器会生成引擎运行时所需的代码,这些代码用于完成 a = 2 这个赋值操作,然后引擎运行时会询问作用域,在当前作用域中是否存在一个 a 变量,如果是,则使用,否则,继续向上层查找,直到全局作用域

引擎在执行时,会对变量进行查询

  • RHS,查询某个变量的值
  • LHS,查询某个变量的容器本身,从而可对其赋值

异常

  • RHS,在所有嵌套作用域都未查询到变量则会抛出 ReferenceError
  • LHS,在全局作用域查找不到时,则会在全局作用域创建该名称的变量,严格模式下则会抛出 ReferenceError

如果查询正确,但操作不合理,如调用非函数变量,则会抛出TypeError

词法作用域

作用域查找会在找到第一个匹配的标识符时停止

多层嵌套作用域中定义同名标识符,可产生遮蔽效应

作用域查找始终从运行所处的内部作用域开始,逐级向上

全局变量会自动成为全局对象(浏览器中的window)的属性,因此可间接通过全局对象属性的引用来访问(在全局变量被同名变量遮蔽时)

无论函数在哪里调用,如何被调用,词法作用域只由声明时的位置决定

词法作用域只查找一级标识符,如foo.bar.baz只会查找foo标识符

欺骗词法

  • eval()接受一个字符串为参数,并运行,会对所在作用域进行修改
  • with根据传递的对象创造一个全新的作用域,并将对象的属性解析到新的作用域

eval和with都会影响性能

函数作用域和块作用域

函数作用域是指属于这个函数的全部变量可以在整个函数的范围内(嵌套也能)使用和复用

let a=2;
function foo() {
    let a=3;
    console.log(a)
}
foo();
console.log(a);

一般声明具名函数,其函数名本身会污染所在作用域

let a=2;
(function foo() {
    let a=3;
    console.log(a);
})();
console.log(a);

立即执行函数名会被绑定在函数内部,即外部无法访问

undefined=true;
(function (undefined) {
    let a;
    if (a===undefined){
        console.log("undfined is safe here!");
    }
})();

该函数保证了内部undefined的值为undefined

JavaScript为函数作用域,该循环中的变量可在外部访问到

for (var i=0;i<2;i++){
    var a=100;
    console.log(i);
}
console.log(i);
console.log(a);

with也是块作用域,从对象中创建的作用域仅在with中有效

try/catch的catch分句也会创建一个块作用域

try {
    undefined();
}
catch (e) {
    console.log(e);
}
console.log(e);

let会将变量隐式的绑定在所在的块作用域,且不会提升声明,在循环中let会将迭代变量绑定到每一次迭代块作用域中

let foo=true;
if (foo){
    let bar=foo*2;
    console.log(bar);
}
console.log(bar);
console.log(str);
let str='he';

块作用域与垃圾回收的相关

function pro(data) {}
let obj={};
pro(obj);
let btn=document.getElementById('div');
btn.addEventListener("click",function (ev) {
    console.log("button");
},false);

由于有回调函数,该作用域不会被回收,无关过程会占据大量内存,可将包装在块作用域中

function pro(data) {}
{
    let obj={};
    pro(obj);
}
let btn=document.getElementById('div');
btn.addEventListener("click",function (ev) {
    console.log("button");
},false);

const也可用于创建块作用域变量,其值为固定

let foo=true;
if (foo){
    const b=3;
    console.log(b);
}
console.log(b);

提升

考虑以下代码

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

console.log(b);
var b;

变量和函数的声明会在代码执行前被处理,上述代码会按如下处理

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

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

只有声明本身会被提升,赋值和其他运行逻辑依旧,且每个作用域都会进行提升

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

函数表达式不会被提升

foo();
var foo=function () {
    console.log('haha');
};

即使具名函数表达式,名称表示符在赋值前也无法在所在作用域中使用

foo();
bar();
var foo=function bar() {
    console.log('hah');
};

上述代码被提升后会被理解为以下形式

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

函数优先,函数声明和变量声明都会被提升,但是函数会先提升

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

后面的函数声明会覆盖前面的

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

作用域闭包

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

在所在作用域执行,bar被封闭在foo的作用域中,更偏向作用域查找

function foo() {
    let a=2;
    function bar() {
        console.log(a);
    }
    bar();
}
foo();

在定义作用域外执行,通常foo在执行后会被销毁,但事实上内部作用域依然存在,bar任然在使用这个作用域,bar对该作用域的引用就叫做闭包

function foo() {
    let a=2;
    function bar() {
        console.log(a);
    }
    return bar;
}
let baz=foo();
baz();

循环和闭包

for (var i=1;i<=5;i++){
    setTimeout(function () {
        console.log(i);
    },1000*i);
}

由于i被声明在同一作用域,循环结束后,当前作用域中只有i=6,IIFE(立即执行函数)可用来创建作用域,但i并没有绑定在作用域中

for (var i=1;i<=5;i++){
    (function () {
        setTimeout(function () {
            console.log(i);
        },1000*i);
    })();
}

间接存储i的值

for (var i=1;i<=5;i++){
    (function () {
        var j=i;
        setTimeout(function () {
            console.log(j);
        },1000*j);
    })();
}

进行改进

for (var i=1;i<=5;i++){
    (function (j) {
        setTimeout(function () {
            console.log(j);
        },1000*j);
    })(i);
}

使用let来劫持块作用域,在块中声明变量

for (var i = 1; i <= 5; i++) {
    let j=i;
    setTimeout(function () {
        console.log(j);
    }, 1000 * j);
}

在循环头使用let声明时,每次迭代都会声明,随后每次跌打都会使用上一次迭代结束时的值来初始化这个变量

for (let i=1;i<=5;i++){
    setTimeout(function () {
        console.log(i);
    },1000*i);
} 

模块

模块模式

  • 有外部封装函数且被调用(每次调用都会创建一个模块实例)
  • 函数内部要返回一个内部函数,才能在私有作用域中形成闭包,并且可访问或修改私有状态
function CoolModule() {
    let something='cool';
    let another=[1,2,3];
    function doSomething() {
        console.log(something);
    }
    function doAnther() {
        console.log(another);
    }
    return {
        doSomething:doSomething,
        doAnther:doAnther
    };
}
foo.doSomething();
foo.doAnther();

当只需要一个实例时,可用单列模式

let foo=(function CoolModule() {
    let something='cool';
    let another=[1,2,3];
    function doSomething() {
        console.log(something);
    }
    function doAnther() {
        console.log(another);
    }
    return {
        doSomething:doSomething,
        doAnther:doAnther
    };
})();
foo.doSomething();
foo.doAnther();

模块也是函数,可接受参数

function CoolModule(id) {
    function identify() {
        console.log(id);
    }
    return {
        identify:identify
    };
}
let foo1=CoolModule(1);
let foo2=CoolModule(2);
foo1.identify();
foo2.identify();

命名将要作为公共API返回的对象

let foo=(function CoolModule(id) {
    function change() {
        publicAPI.identify=identify2;
    }
    function identify1() {
        console.log(id);
    }
    function identify2() {
        console.log(id.toUpperCase());
    }
    let publicAPI={
        change:change,
        identify:identify1
    };
    return publicAPI;
})('foo module');
foo.identify();
foo.change();
foo.identify();

现代模块机制

let MyModules=(function Manager() {
    let modules={};
    function define(name,deps,impl) {
        for (let i=0;i<deps.length;i++){
            deps[i]=modules[deps[i]];
        }
        modules[name]=impl.apply(impl,deps);
    }
    function get(name) {
        return modules[name];
    }
    return {
        define:define,
        get:get
    };
})();
MyModules.define('bar',[],function () {
    function hello(who) {
        return "let me introduce: "+who;
    }
    return {
        hello:hello
    };
});
MyModules.define('foo',['bar'],function (bar) {
    let hungry='hippo';
    function awesome() {
        console.log(bar.hello(hungry).toUpperCase());
    }
    return {
        awesome:awesome
    };
});
let bar=MyModules.get('bar');
let foo=MyModules.get('foo');
console.log(bar.hello('hippo'));
foo.awesome();

未来的模块机制

bar.js

function hello(who){
    return "Let me introduce: "+who;
}
export hello;

foo.js

import hello from "bar";
var hungry="hippo";
function awesome(){
    console.log(hello(hungry).toUpperCase());
}
export awesome;

baz.js

module foo from "foo";
module bar from "bar";
console.log(bar.hello("rhino"));
foo.awesome();

动态作用域

动态作用域并不关心函数和作用域时如何声明以及在何处声明,只关心从何处调用,也就是说作用域链是基于调用栈,而不是代码的作用域嵌套

function foo() {
    console.log(a);
}
function bar() {
    let a=3;
    foo();
}
let a=2;
bar();

词法作用域让foo()中的a通过RHS引用了全局作用域的a,所以输出2

而动态作用域foo()无法找到a的变量引用时,会顺着调用栈,在调用foo的地方查找a,就会i输出3

箭头函数(=>)

写法如下

let foo=a=>{
    console.log(a);
};
foo(2);

但箭头函数不仅仅是function的简写,更重要的是解决丢失this的绑定

let obj={
    id:'awesome',
    cool:function () {
        console.log(this.id);
    }
};
let id="not cool";
obj.cool();
setTimeout(obj.cool,100);

cool函数丢失了同this的绑定,利用词法作用域

let obj={
    count:0,
    cool:function () {
        let self = this;
        if (self.count<1){
            setTimeout(function () {
                self.count++;
                console.log("awesome?");
            },100);
        }
    }
};
obj.cool();

利用箭头函数,在涉及this绑定时,箭头函数会放弃普通this绑定规则,而是用当前的词法作用域覆盖this本来的值

let obj={
    count:0,
    cool:function () {
        if (this.count<1){
            setTimeout(()=>{
                this.count++;
                console.log("awesome?");
            },100);
        }
    }
};
obj.cool();

bind方式

let obj={
    count:0,
    cool:function () {
        if (this.count<1){
            setTimeout(function () {
                this.count++;
                console.log("more awesome");
            }.bind(this),100);
        }
    }
};
obj.cool();

猜你喜欢

转载自blog.csdn.net/qq_38904449/article/details/81257633
今日推荐