天天看點

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就失去了意義,就将函數名省略了,就形成了現在的立即指向函數。