1. 程式人生 > 其它 >JavaScript 中的 this 與閉包詳解

JavaScript 中的 this 與閉包詳解

技術標籤:Web

JavaScript 中的 this

一、什麼是 this ?


在 JavaScript 中 this 關鍵字一般指的是 函式呼叫時 所在的 環境上下文 ,儲存了 環境上下文物件的記憶體地址 ,根據函式的呼叫的方式不同 ,其 this 會指向不同的物件 ,我們可以通過 this 關鍵字在函式內部中操作其指向的物件 ,看看下面的例子。

// 定義 person 物件
var person = {
	name: 'momo',
	age: 18,
	gender: '男',

	say: function() {
		console.log('我的名字是 ' + this.
name + ' ,今年' + this.age + '歲'); } }; // 全域性作用域中定義一個 say 函式 function() say { console.log(this); } person.say(); // 列印:我的名字是 momo ,今年18歲 say(); // 等價於 window.say() 列印:Window

上面的例子中 ,通過 person 物件呼叫 say 函式的時候 ,函式內部中的 this 指的就是呼叫它的 person 物件 ,因此可以通過 this 來訪問 person 物件的 name 屬性和 age 屬性 。直接呼叫全域性作用域中的 say 函式的時候等價於 window.say()

因此 ,全域性作用域中的 say 函式中的 this 指向的就是 window 物件 。

function fn() {
	console.log(this.age);
}

var obj = {
	age: 18,
	fn: fn
};

fn(); // undefined
obj.fn(); // 18

二、建構函式中的 this


// 建構函式的定義
function Person(name, age, gender) {
	this.name = name;
	this.age = age;
	this.gender = gender;
	
	this.say = function()
{ console.log('我的名字是 ' + this.name + ' ,今年' + this.age + '歲'); } } // 通過建構函式建立 person 物件 var person = new Person('momo', 18, '男'); person.say(); // 列印:我的名字是 momo ,今年18歲

建構函式中的 this 的用法與上面普通函式的用法沒多少區別 ,這裡主要講一下建構函式是如何建立一個物件的 。在使用 new 關鍵字呼叫建構函式後 ,會在堆記憶體空間中建立一個新的物件 ,然後建構函式中的 this 就儲存了堆空間這個新的物件記憶體地址 ,最後會預設返回這個 this 。上面的例子中將建構函式的返回值 ,也就是 this 儲存的記憶體地址賦值給了 person 變數 。


三、修改函式中的 this 指向


在 JavaScript 中還提供了修改函式中 this 指向的方法 ,可以通過 call()apply()bind() 函式來修改某個函式呼叫時 this 指向的物件 。

/* =============== call 與 apply ============ */
function say(a, b) {
    console.log('我是' + this.value + ' ' + a + ',' + b);
}

var red = {
    value: '紅色',
    redSay: say
};

var green = {
    value: '綠色',
    greenSay: say
};

red.redSay(1, 2); // 我是紅色 1,2
green.greenSay(3, 4); // 我是綠色 3,4

// 將 redSay 函式中的 this 指向改為 green 物件
red.redSay.call(green, 1, 2); // 我是綠色 1,2
red.redSay.apply(green, [3, 4]); // 我是綠色 3,4

從上面的例子可以看出來 call()apply() 是由函式物件呼叫的 ,傳入的第一個引數就是指定該函式物件中 this 的指向 ,後面傳入的引數就是通過 call / apply 呼叫指定函式時傳入的引數 ,call()apply() 的主要差異就是 call() 傳入第一個引數以後 ,後面傳入的所有引數都是呼叫指定函式時傳入的引數 ,apply() 則將這些引數封裝到了一個數組中 。

關於 call()apply() 的用法基本相同 ,下面來介紹一下 bind() ,先看一個例子 。

window.name = 'window';

var person = {
    name: 'momo',
    say: function(a, b) {
        console.log('我的名字是' + this.name + ' ' + a + ',' + b);
    }
}
var mSay = person.say;

// 丟失了 person 物件的 this
mSay(1, 2); // 我的名字是window 1, 2

// 重新給 mSay 的 this 繫結為 person 物件 
mSay = mSay.bind(person);

// 此時 mSay 中的 this 就是 person 物件了
mSay(1, 2); // 我的名字是momo 1, 2

// 下面的寫法與上面等價
mSay.bind(person, 1, 2)();
mSay.bind(person)(1, 2);

從上面的例子可以看出使用 bind() 來修改函式的 this 的時候並不會執行該函式 ,而是 返回一個新的函式物件 ,這個新的函式物件中的 this 被修改為了指定的物件 ,其餘的函式體內部程式碼與修改前的一樣 。


四、其他需要注意的地方


  1. 直接在全域性作用域中使用 this ,其表示的是 window 物件 。

  2. 在 ES6 中箭頭函式內部的 this 指向的是箭頭函式定義時的上下文物件 ,不由呼叫它的物件來決定 。

    // 直接在全域性作用域中用 this 賦值
    var a = this;
    console.log(a); // Window 物件
    
    
    window.name = 'window';
    var obj = {
        name: 'obj',
        fn: () => void console.log(this.name)
    }
    obj.fn(); // window
    

五、練習

var obj = {
  foo: function () { console.log(this.bar) },
  bar: 1
};

var foo = obj.foo;
var bar = 2;

obj.foo(); // 1
foo(); // 2
window.name = 'window';
var obj = {
	name: 'obj',
	fn1: function() {
		console.log(this.name);
	},
	fn2: function() {
		function fn() {
			console.log(this.name);
		}
		fn();
	},
	fn3: () => {
		console.log(this.name);
	}
}

obj.fn1(); // obj
obj.fn2(); // window
obj.fn3(); // window
obj.fn3.call(this); // window

var fn4 = obj.fn1;
fn4(); // window


JavaScript 中的閉包機制

一、什麼是閉包?


先看下面一個場景 ,fn1 函式中嵌套了一個 fn2函式 ,為了方便後面的描述 ,這裡把 fn1 函式稱為外部函式 ,fn2 函式稱為內部函式 。

function fn1() {
    var a = 3;

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

通過 Chrome 除錯一下 ,檢視 fn2 函式物件中的內部有哪些資料 。

可以看見 fn2 內部函式中存在一個 Closure 物件 ,其儲存了外部函式中定義的變數 a 的資料 ,實際上這個 Closure 物件就是 fn2 內部函式的閉包物件 。這裡直接給出結論 ,閉包是一個物件 ,其存在於內部函式物件中 ,儲存了內部函式所使用的外部函式中定義的資料

二、閉包的產生條件


  1. 函式巢狀 。
  2. 內部函式使用了在外部函式中定義的資料 。
  3. 指執行了外部函式 。

詳細說明一下閉包物件產生的整個流程 。首先在一個 函式巢狀 的場景下 ,並且 內部函式使用了外部函式定義的資料 ,然後再 執行外部函式 ,當代碼執行到 內部函式定義完畢 時 ,此時內部函式中就已經生成了一個閉包物件 ,其 儲存了內部函式使用的外部函式中定義的資料


三、閉包的生命週期


  1. 閉包的產生:在 函式巢狀 場景下 , 內部函式使用了在外部函式中定義的資料 ,在執行外部函式時 ,內部函式定義完畢 ,此時內部函式中就產生了閉包物件 。
  2. 閉包的死亡:在 堆區的內部函式物件沒有被棧區的變數引用 時 ,此時堆區的內部函式物件就會被 GC 當作垃圾資料回收 ,同時存在於內部函式物件中的閉包物件就會死亡 。
function fn1() {
      var a = 3;
      
	  // 當 fn2 函式物件定義完畢時 ,其內部產生了閉包物件
      function fn2() {
          console.log(a);
      }
      return fn2;
  }

// 呼叫 fn1 函式 ,將 fn2 函式物件的記憶體地址賦值給 fn3 物件 
var fn3 = fn1();
// 中斷 fn3 於 fn2 物件之間的引用 ,fn2 被 GC 回收 ,閉包物件死亡
fn3 = null;

四、閉包的應用


function fn1() {
    let a = 3;

    function fn2() {
        a++;
        console.log(a);
    }
    return fn2;
}

var fn = fn1();
fn(); // 4
fn(); // 5

在 fn1 函式執行完畢後 ,其內部的區域性變數 a 已經被釋放 ,但是由於閉包機制的存在 ,fn2 函式物件儲存了這個區域性變數的資料 ,延長了區域性資料的存活時間 。

function fn1(time) {
    let i = 1;

    setInterval(function() {
        i++;
        console.log(i);
    }, time);
}

fn1(2000);

定時器的回撥函式中的閉包物件儲存了 fn1 外部函式中的區域性變數 i 的資料 ,因此在 fn1 函式呼叫完畢並且釋放完記憶體空間後 ,仍能夠對區域性變數 i 的資料進行累加 。


五、閉包的優缺點


優點

  1. 區域性變數資料被儲存起來沒有被銷燬 ,隨時可以被呼叫 ,延長了區域性變數資料的存活時間
  2. 只有函式內部的子函式才能讀取區域性變數 ,可以避免全域性汙染

缺點

  1. 函式執行完後 ,函式內部的區域性變數資料儲存在閉包物件中 ,佔用記憶體時間長,如果不及時釋放會影響效能 。
  2. 容易導致記憶體洩漏 。

六、關於記憶體溢位和記憶體洩漏


  1. 記憶體溢位:記憶體溢位是一種程式執行錯誤 ,當程式的執行需要的記憶體超過了剩餘的記憶體時就會丟擲記憶體溢位的錯誤 。
  2. 記憶體洩漏:佔用的記憶體沒有被及時釋放 。

注意:記憶體洩漏累積多了就容易出現記憶體溢位的錯誤 ,比如:意外的全域性變數 ,沒有關閉的迴圈定時器 ,閉包 。

防止記憶體溢位的方法:

  1. 完成需求後記得 clear 定時器 。
  2. 減少全域性變數的使用 。
  3. 儘量使用原型物件去定義函式 。
  4. 閉包執行完成後,將引用的區域性變數賦值為 null 。