淺談JS作用域、this及閉包

original_galaxy發表於2018-08-20

作用域可以理解為環境上下文,包含了變數、函式宣告、引數等。在es6之前,JS使用的是全域性作用域和函式作用,無塊級作用域。

JS有自己特有的作用域鏈,函式中宣告的變數在整個函式中都是有定義的。查詢一個變數時,先在變數所在函式體內找,找不到向更外層函式找,一直到全域性變數(注:全域性變數都是window物件的屬性)。程式碼寫出時就定義好了作用域,比如誰巢狀在誰裡面。

注意for、if、else 是不能創造作用域的。

// 只有一個popup函式級作用域,變數i、j、k在整個popup函式體內都是全域性的
function popup() {
    var i = 0;
    if(true) {
        var j = 0;
        for(var k = 0; k < 3; k++) {
            console.log(k); // 分別輸出 0 1 2
        }
        console.log(k); // 輸出3
    }
    console.log(j); // 輸出0
}
複製程式碼

let的增加,引入了塊級作用域,相比var的一些好處:

  • 避免了變數宣告提升,防止變數的覆蓋/洩露
// 雖然此處引用a在宣告a之前,但並未報錯,即變數提升
console.log(a); // undefined 
var a = 1;

// 上面程式碼可間接理解成如下邏輯
var b;
console.log(b);  // undefined
b = 1;

// 一函式體內任意位置宣告的函式或變數,都會被提升到函式體內最頂層
// 形參不會被重新定義,且同名的優先順序 函式>形參>變數
var c = 1;
function run(x, y, z, w) { // 形參會被新增到函式的作用域中
    console.log(c); // 內部有c的宣告,所以輸出'undefined',而不是1
    var c = 'runnerman';
    console.log(c); // 輸出'runnerman'
    
    console.log(x); // 
    var x = 5; // x=5被執行
    function x() { // 被提升到了作用域頂部
        console.log('x coming');
    }
    console.log(x); // 5
    
    console.log(y); // parma2
    var y = 10; // var y被忽略,y=10被執行
    console.log(y); // 10
    
    console.log(z); // param3
    var z = function z() { // var z被忽略
        console.log('z coming');
    };
    console.log(z); // 
    
    console.log(w); // 
    function w() {
        console.log('w coming');
    };
    w = 20;
    console.log(w); // 20
}
run('param1', 'param2', 'param3', 'param4');
/* 輸出:
undefined
runnerman
function x() {
    console.log('x coming');
}
5
param2 
10
param3
function z() {
    console.log('z coming');
}
function w() {
    console.log('w coming');
}
20
*/

// 但如此使用let,便會報錯
console.log(d); // Uncaught ReferenceError: d is not defined
let d = 1;

var e = 90;
var e = 900; // 可以,會被覆蓋

// let不可重複宣告
let f = 90;
let f = 900; // Uncaught SyntaxError: Identifier 'f' has already been declared
複製程式碼
  • TDZ暫時性死區:繫結塊級作用域,不受外部影響,封閉作用域
for(var i=0;i<3;i++) {
    setTimeout(function() {
        console.log(i)
    }, 1000)
}

// 結果:3,3,3

for(let j=0;j<3;j++) {
    setTimeout(function() {
        console.log(j)
    }, 1000)
}

// 結果:0,1,2
複製程式碼

關於this的指向

  • this總是指向函式的直接呼叫者(而非間接呼叫者)所在環境
  • 如果有new關鍵字,this指向new出來的那個物件
  • 在事件機制中,this指向觸發這個事件的物件(除了IE的attachEvent中的this總是指向全域性物件window)
function fight() {
    console.log(this) // Window
}
// 此處相當於Window呼叫了fight
fight()

var ironman = {
    name: "Tony Stark",
    fly: function() {
        console.log(this.name + ' is flying')  // this === ironman
    }
}
ironman.fly() // Tony Stark is flying

function Superhero(name, power) {
    this.name = name // this指向spiderman
    this.power = power
    //return this
}

// 首先new欄位會建立一個空的物件,然後呼叫apply()函式,將this指向這個空物件
var spiderman = new Superhero('spiderman', 'jumping')
複製程式碼
  • 更改this指向
var name = 'anyone', age = '30'
var ironman = { 
    name: "Tony Stark", 
    imAge: this.age, 
    run: function(skill) {
        console.log(this.name + " is " + this.age + ', ready to ' + skill)
    } 
}

// ironman為全域性變數,此時的this指向為Window
console.log(ironman.imAge) // 30

// 此時函式中的this指向ironman
ironman.run('fly') // Tony Stark is undefined, ready to fly

// call,apply,bind第一個引數都是this指向的物件
// call和apply如果第一個引數指向null或undefined時,那麼this會指向Window物件
// call,apply都是改變上下文中的this,並立即執行;bind方法可隨後手動呼叫
var starlord = {name: "dude", age: 13}
ironman.run.call(starlord, "dance") // dude is 13, ready to dance
ironman.run.apply(starlord, ["dance"]) // dude is 13, ready to dance
ironman.run.bind(starlord, "dance")() // dude is 13, ready to dance
複製程式碼
  • 箭頭函式中的特殊情況
var globalObject = this;
var foo1 = (() => this); // 箭頭函式:宣告時已確定了指向
var foo2 = function() { return this }; // 執行時才能確定指向
console.log(foo1() === globalObject); // true
console.log(foo2() === globalObject); // true

var obj = {foo1: foo1, foo2: foo2};
console.log(obj.foo1() === globalObject); // true
console.log(obj.foo2() === obj); // true 指向呼叫其的物件

console.log(foo1.call(obj) === globalObject); // true
console.log(foo2.call(obj) === obj); // true

foo1 = foo1.bind(obj);
foo2 = foo2.bind(obj);
console.log(foo1() === globalObject); // true
console.log(foo2() === obj); // true
複製程式碼

閉包(Closure)

  • 可以簡單理解成讀取所在函式內部其它變數的函式,定義在函式內部的函式,即內部函式;或者說內部函式和其詞法作用域形成了一個閉包。
  • 連通起函式外部與函式內部的媒介
function Printer() {
  var count = 0;
  this.print = function() { // 引用了函式區域性變數count
    count++; 
    console.log(count);
  };
}
var p = new Printer();
p.print(); // 1 相當於從外部引用了函式內部的區域性變數
p.print(); // 2 此處也說明了count一直在記憶體中,並未在print呼叫後清除,原因正是因為count被函式外部所引用的關係
複製程式碼

一般用於:

  • 讀取函式內部的變數
  • 將變數保持在記憶體中

注意:濫用閉包會導致函式中的變數都被儲存在記憶體中,記憶體消耗很大,導致網頁效能問題,IE中可能導致記憶體洩露。所以在退出函式之前,最好將不使用的區域性變數全部清除。

相關文章