ABCDdouyaer / jiraiya.github.io

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

你不知道的JavaScript上 之【作用域和闭包】

ABCDdouyaer opened this issue · comments

作用域和闭包

作用域是什么

1.源代码的编译在执行前会经历三个步骤

分词/词法分析: 将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元

解析/语法分析: 将词法单元流转换成一个由元素逐级嵌套所组成的代表了程序语法结构的树

代码生成: 将AST转换为可执行代码的过程称为代码生成

2.左查询和右查询

RHS查询与简单地查找某个变量的值别无一二(谁是赋值操作的源头);

LHS查询则是试图找到变量本身的容器(赋值操作的目标是谁);

词法作用域

1.概念

作用域分为动态作用域(Bash脚本、Perl中的一些模式)和词法作用域;

词法作用域就是定义在词法阶段的作用域

function foo(a){
    var b = a * 2;
    
    function bar(c){
        console.log(a, b, c);
    }

    bar(b * 3)
}

foo(2)

包含在整个作用域的标识符:foo

包含在foo创建的作用域:a, b, bar

包含在bar所创建的作用域:c

2.欺骗词法(副作用是引擎无法在编译时对作用域查找进行优化)

eval 在执行eval之后的代码时候,引擎并不知道前面代码会以动态形式插入,并对词法作用域的环境进行修改(严格模式,eval无法修改所在作用域)

function foo(str, a){
    eval(str);//欺骗
    console.log(a, b);
}

foo("var b = 3;", 1); // 1, 3

function bar(str){
    'use strict'
    console.log(c);
}
bar("var c = 2");//ReferenceError: c is not defined

with 当对一个对象中的多个属性进行修改可以避免重复引入对象,但是存在副作用,当该对象没有该属性,会将变量泄露到全局

var obj = {
    a: 1,
    b: 2,
    c: 3,
}

obj.a = 2;
obj.b = 3;
obj.c = 4;

with(obj){
    a = 5,
    b = 6,
    c = 7;
    d = 8;
}

console.log(Object.entries(obj));
console.log(global.d)
// [ [ 'a', 5 ], [ 'b', 6 ], [ 'c', 7 ] ]
// 8

函数作用域和块作用域

1.立即执行函数IFFE好处

  • 避免污染全局作用域

  • 将需要的window等以参数形式传入并改变名字使内部代码看起来更加清晰

  • 解决undefined默认值被错误覆盖

var a = 2;
(function IIFE(global, undefined){
    var a = 3;
    console.log(a); //3
    console.log(global.a); //2
})(window, undefined)
  • 倒置代码运行顺序,将运行的函数放在第二部分,在IIFE执行之后当做参数传入,这种模式在UMD项目中广泛使用,更易理解
var a = 2;
(function IIFE(def){
    def(window);
})(function def(global){
    var a = 3;
    console.log(a); //3
    console.log(global.a); //2
})

提升

1.概念

包括变量和函数在内的所有声明都会在任何代码被执行之前首先被处理,只有声明本身会被提升,而赋值或其他运行逻辑会留在原地,函数优先级高于变量

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

等同于


function foo(){
    cosnole.log(1);
}
foo();//1

foo = function(){
    console.log(2)
}

2.一些不应该犯得错误

  • 避免重复声明函数
foo(); //3

function foo(){
    console.log(1);
}   

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

function foo(){
    console.log(3);
}
  • 避免在块内部声明函数
foo(); //3

var a = true;
if(true){
    function foo(){
        console.log(2)
    }
}else{
    function foo(){
        console.log(3)
    }
}

作用域闭包

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

function foo(){
    var a = 1;
    function bar(){
        console.log(a)
    }
    return bar;
}

foo()();//1
var baz;
function foo(){
    var a = 1;
    function bar(){
        console.log(a)
    }
    baz = bar;
}

foo();
baz();//1

在定时器、事件监听器、Ajax请求、跨窗口通信、Web Workers或者任何其他异步或者同步任务中,只要使用了毁掉函数,实际上就是在使用闭包

2.模块实际也是闭包

  • 必须有外部封闭函数,该函数至少被调用一次(每次调用都会创建一个新的模块实例)

  • 封闭函数必须返回至少一个内部函数,这样内部函数才能在私有作用域中形成闭包,并且可以访问或者修改私有状态

例如:单例模式

var foo = (function(){
    var something = 'cool';
    var another = [1, 2, 3];

    function dosomething(){
        console.log(something)
    }

    function doAnother(){
        console.log(another.join('!'));
    }

    return {dosomething, doAnother};
})();

foo.dosomething();
foo.doAnother();

现代模块机制

var myModules = (function(){
    var modules = {};

    function define(name, deps, impl){
        for(var 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, get}
})()

myModules.define('bar', [], function(){
    function hello(who){
        return 'Let me introduce: ' + who;
    }
    return{hello};
})

myModules.define('foo', ['bar'], function(bar){
    var hungry = 'hippo';
    function awesome(){
        console.log(bar.hello(hungry).toUpperCase());
    }
    return {awesome}
})

console.log(myModules.get('bar').hello('Mr Wang'));
console.log(myModules.get('foo').awesome())
//Let me introduce: Mr Wang
//Let me introduce: Mr Wang