天天看点

js笔记——作用域、作用域链精解、闭包、立即执行函数’

作用域、作用域链精解

运行期上下文:当函数执行时,会创建一个称为执行期上下文的内部对象。一个执行期上下文定义了一个函数执行的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,它所产生的执行上下文被销毁。
查找变量:从该函数的作用域链的顶端依次向下查找
[[scope]]:每个javascript函数都是一个对象,对象中有些属性我们可以访问,但有些不可以,这些属性仅供javascript引擎存取,[[scope]]就是其中一个。[[scope]]指的就是我们所说的作用域,其中储存了运行期上下文的集合。
作用域链:[[scope]]中所储存的执行期上下文对象的集合,这个集合呈链链接,我们把这这种链式连接叫做作用域链。
function a() {
    function b() {
        var bb = 234;
    }
    var aa = 123;
    b();
}
var global = 100;
a();
           
js笔记——作用域、作用域链精解、闭包、立即执行函数’
js笔记——作用域、作用域链精解、闭包、立即执行函数’

b函数在被创建的时候直接获取到a函数作用域链的引用

js笔记——作用域、作用域链精解、闭包、立即执行函数’
js笔记——作用域、作用域链精解、闭包、立即执行函数’

问题:b函数中a的AO和a函数中a的AO是不是同一个AO?

可以做个测试:

function a() {
    function b() {
        var bb = 234;
        aa=111;//在函数b中把a函数中的变量aa的值修改了
    }
    var aa = 123;
    b();
    console.log(aa);//控制台打印出  111  说明是同一个AO    
}
var global = 100;
a();
           

例子:

function a() {
    function b() {
        var bbb = 234;
        console.log(aaa);
    }
    var aaa = 123;
    return b;
}
var glob = 100;
var demo = a();
demo();//123
           

a函数在执行的时候遇到b函数的创建,于是b函数的作用域链和a函数的作用域链指向同一块区域。如图:

js笔记——作用域、作用域链精解、闭包、立即执行函数’

a函数执行完毕之后,a函数的执行上下文被销毁,但是b函数的还在,如图:

js笔记——作用域、作用域链精解、闭包、立即执行函数’

应用:

function a() {
    var num = 100;
    function b() {
        num++;
        console.log(num);
    }
    return b;
}
var demo = a();
demo();//101
demo();//102
           

闭包

当内部函数被保存到外部时,将会生成闭包。闭包会导致原有作用域链不释放,造成内存泄露。

  • 引例
function test() {
    var arr = [];
    for (var i = 0; i < 10; i++) {
        arr[i] = function () {
            document.write(i + " ");
        }
    }
    return arr;
}
var myArr = test();
for (var j = 0; j < 10; j++) {
    myArr[j](); //10 10 10 10 10 10 10 10 10 10
}
           

首先,test()函数执行的时候创建自己的AO对象,执行完毕之后test()的AO中的i为10,注意此时里面的函数还没有开始执行,所以还没有创建自己的AO对象,当在下面的for循环中执行myArr[i]函数时,函数创建自身的AO对象,指向test函数的AO对象,此时test函数的AO对象里面的i为10,所以myArr[i]函数的面打印的i为10。

但是如果要打印出来的是0 1 2 3 4 5 6 7 8 9,该怎么做?

如下,在函数表达式外面嵌套一层立即执行函数,再次执行myArr[i]函数的时候,myArr[i]函数创建自己的AO并继承立即执行函数的AO,立即执行函数中的AO中的j在每次执行for循环的时候已经被改变,所以myArr[i]打印的是立即执行函数中的变量j的值,而不是test函数中i的值。

function test() {
    var arr = [];
    for (var i = 0; i < 10; i++) {
        (function (j) {
            arr[j] = function () {
                document.write(j + " ");
            }
        }(i));
    }
    return arr;
}
var myArr = test();
for (var j = 0; j < 10; j++) {
    myArr[j](); //0 1 2 3 4 5 6 7 8 9
}
           

闭包的作用

  1. 实现公有变量

    例子:实现累加器

function add() {
    var count = 0;
    function demo() {
        count++;
        console.log(count);
    }
    return demo;
}
var counter = add();
counter(); // 1
counter(); // 2
counter(); // 3
counter(); // 4
           
  1. 可以做缓存(存储结构)
function test() {
    var num = 100;
    function a() {
        num++;
        console.log(num);
    }
    function b() {
        num--;
        console.log(num);
    }
    return [a, b];
}
var arr = test();
arr[0](); //101
arr[1](); //100
// 需要明白的是num在函数test()的AO中,而函数a和函数b操作的是同一个AO,即同一个mun,所以会出现上面的结果
           

例子:

function eater() {
    var food = "";
    var obj = {
        eat: function () {
            console.log('I am eating ' + food);
            food = '';
        },
        push: function (myFood) {
            food = myFood;
        }
    }
    return obj;
}
var eater1 = eater();
eater1.push('banana');
eater1.eat(); //I am eating banana
//这里多个函数和同一个函数形成闭包,操作的是同一块区域
           
  1. 实现实例封装,属性私有化
function Deng(name, wife) {
     var prepareWife = "xiaozhang";
     this.name = name;
     this.wife = wife;
     this.divorce = function () {
         this.wife = prepareWife;
     }
     this.changeWife = function (target) {
         prepareWife = target;
     }
     this.sayPrepareWife = function () {
         console.log(prepareWife);
     }

 }

 var deng = new Deng("deng","xiaoliu");
 console.log(deng.wife);// xiaoliu
 deng.divorce();
 console.log(deng.prepareWife);// undefined
 deng.sayPrepareWife();// xiaozhang
 /*
 类的定义里面没有prepareWife这个属性,但是在实例化对象的时候
 可以利用函数操作prepareWife属性,从而实现属性的私有化
 */
           
  1. 模块化开发,防止污染全局变量

立即执行函数

定义:此函数没有声明,在一次执行过后即释放。适合做初始化工作。
特点:执行后立即被释放,除此之外和其他函数没有区别,也可以有形式参数和返回值。
// 形式:先写一对大括号,在里面定义一个匿名函数,后面加一个小括号
(function () {
    var a = 123;
    var b = 234;
    console.log(a + b);
}())
//控制台打印出357
           

可以用变量来接收立即执行函数的返回值

var res = (function () {
    return 'hello';
}());
console.log(res);// hello
           

拓展:

// 立即执行函数有两种定义方式:
// (function () {}()); W3C建议使用这种定义方式
// (function () {})();

// 只有表达式才能被执行符号执行
function test() {
    var a = 123;
}()
// 报错:Uncaught SyntaxError: Unexpected token ),
// 因为此处为函数声明,不属于表达式,换成函数表达式就会被执行,如下
var test = function () {
    console.log('hello');
}()// hello

//表达式被执行之后就会忽略表达式的名字
// 例如,当函数没有被执行的时候,可以打印出test的值
var test = function(){
    console.log('a');
};
console.log(test);//ƒunction (){console.log('a');}
// 当函数执行完之后
var test2 = function(){
    console.log('a');//a 
}();
console.log(test2);//undefined

// 利用运算符(+ - && ||可以,但是* /不可以,因为这里的+ -是正负号的意思,而不表示加减)可以将函数声明转化为表达式,然后函数名字就会失效
+ function test(){
    console.log('a');
}
console.log(test);// Uncaught ReferenceError: test is not defined
// 同样,转换后的函数可以添加小括号让其立即执行
+ function test(){
    console.log('a');
}();// a

// 由此引申出来()也算是运算符,()里面的函数也可以转化为表达式,例如(function test(){}),既然是表达式,就可以被执行,后面加上括号就会被执行,如下
(function test(){
    console.log('a');
})();// a
// 由于在有多级括号的时候先执行最外面的括号,所以外面的括号也可以放在里面,于是就有了下面的写法:
(function test(){
    console.log('a');
}());// a
// 又因为在外界不能引用函数名test,所以函数名test就失去了意义,就将函数名省略了,就形成了现在的立即指向函数。