第6章 JS基礎-作用域和閉包【三座大山之二,不會閉包,基本不會通過】

返回章節目錄

目錄

1.作用域和自由變量

作用域

自由變量

2.閉包

3.this

4.練習題

1.手寫bind函數

2.做一個簡單的cache工具

3.創建10個標籤,點擊的時候彈出對應的序號


1.作用域和自由變量

作用域

一共4個紅框,分別代表了a,a1,a2,a3這些變量的合法使用範圍

作用域分爲

全局作用域(不講)

函數作用域(不講)

塊作用域(ES6新增)

塊作用域這裏let和const都一樣,超出範圍報錯

 

自由變量

一個變量在當前作用域沒有定義,但被使用了,則向上級作用域一層一層依次尋找,直至找到爲止,如果到全局作用域都沒找到,則報錯 xx is not defined。可以這樣理解,凡是跨了自己的作用域的變量都叫自由變量。

 

2.閉包

閉包是作用域應用的特殊情況,有兩種表現:

1.函數作爲參數被傳遞

2.函數作爲返回值被傳遞

簡單理解:當一個嵌套的內部(子)函數引用了嵌套的外部(父)函數的變量(函數)時, 就產生了閉包

function create() {
    const a = 100
    return function () {
        console.log(a)
    }
}

const fn = create()
const a = 200
fn() // 100

這段代碼打印100,這個so easy

function print(fn) {
    const a = 200
    fn()
}
const a = 100
function fn() {
    console.log(a)
}
print(fn) // 打印多少呢

這段代碼打印100

如果這2個例子很輕鬆看出結果說明閉掌握的比較好

說一下第二個例子爲什麼不是200

所有的自由變量的查找,是在函數定義的地方向上級作用域查找,不是在執行的地方!

所以這裏函數定義的地方往上查找就是100

進一步說明是在函數定義的地方向上級作用域查找,找不到就直接報錯!如果在找不到的情況接着在執行的地方查找就會打印200,但是這裏不打印,說明不會在執行的地方查找。

關於閉包和原型更多更全系列推薦下面博客

深入理解javascript原型和閉包(完結)

 

3.this

this的應用場景一般如下

1.在普通函數使用

2.使用call、apply、bind

3.在對象方法中被調用

4.在class方法中調用

5.在箭頭函數中使用

this的取值是在函數執行的時候確定的,不是在函數定義的時候確定的,這個規則適用於上面所有場景

來看看例子

這裏直接fn1()打印的this是window

調用call之後打印是this是這個對象{ x: 100}

調用bind之後不會直接執行,返回另外一個函數,執行這個函數,this指向bind的對象{ x : 200 }

sayHi()裏面的this就是當前對象,這裏setTimeout裏面的函數和普通函數一樣,this就是window

箭頭函數中的this是上級作用域的this的值,嵌套setTimeout也是一樣,因爲最外層setTimeout的this是上級作用域的this,所以最內層的setTimeout中的this還是和外層的一樣,即上級作用域的this

class裏面的this就是指的當前實例對象

 

4.練習題

1.手寫bind函數

如果要實現一下代碼的功能,將其中的bind重寫一下該如何實現呢?先來看看api的實現結果

function fn1(a, b, c) {
    console.log('this', this)
    console.log(a, b, c)
    return 'this is fn1'
}

const fn2 = fn1.bind({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)

運行結果: 

手寫bind函數如下:

// 老師模擬 bind的代碼,我覺得需要一點優化
/*Function.prototype.bind1 = function () {
    // 將參數拆解爲數組
    const args = Array.prototype.slice.call(arguments)

    // 獲取 this(數組第一項)
    const t = args.shift() // 獲取並刪除第一個對象

    // fn1.bind(...) 中的 fn1
    const self = this // 誰調用函數this就是誰

    // 返回一個函數
    return function () {
        return self.apply(t, args)
    }
}*/
// 我自己模擬 bind函數
Function.prototype.bind1 = function () {
    // 獲取 this(數組第一項)
    const t = arguments[0]

    // fn1.bind(...) 中的 fn1
    const self = this // 誰調用函數this就是誰

    // 將參數拆解爲數組
    const args = Array.prototype.slice.call(arguments, 1)


    // 返回一個函數
    return function () {
        return self.apply(t, args)
    }
}

function fn1(a, b, c) {
    console.log('this', this)
    console.log(a, b, c)
    return 'this is fn1'
}

const fn2 = fn1.bind1({x: 100}, 10, 20, 30)
const res = fn2()
console.log(res)

當然運行結果和原生bind是一樣的。爲什麼我說需要優化一下老師的代碼?

老師代碼步驟先獲取新數組(arguments轉化爲數組),然後再去刪除第一個元素(這個過程很耗時,因爲除了第一個元素後面依次往前移動,最後長度減少1)

我的bind就是先獲取第1個參數,方便後面apply綁定用,然後再獲取新數組(arguments從下標1開始往後),這樣就少去後面依次往前移動的步驟。

這裏涉及到了很多api,這個需要積累熟悉,可以自尋官網文檔

 

來看看官網文檔Polyfill寫法其實和上面差不多,兼容性更好,這段代碼可以使你的 bind() 在沒有內置實現支持的環境中也可以部分地使用bind

// Does not work with `new funcA.bind(thisArg, args)`
if (!Function.prototype.bind) (function(){
  var slice = Array.prototype.slice;
  Function.prototype.bind = function() {
    var thatFunc = this, thatArg = arguments[0]; // 取第一個參數,目標是this指向的第一個參數
    var args = slice.call(arguments, 1); // 從下標1開始將arguments類數組轉換成數組
    if (typeof thatFunc !== 'function') {
      // closest thing possible to the ECMAScript 5
      // internal IsCallable function
      throw new TypeError('Function.prototype.bind - ' +
             'what is trying to be bound is not callable');
    }
    return function(){
      var funcArgs = args.concat(slice.call(arguments)) //這個arguments是return的匿名函數的,和上面的arguments沒有關係,不要混淆
      return thatFunc.apply(thatArg, funcArgs);// 最後返回的函數中return this.apply(第一個參數,除了第一個參數之後的參數(可能有bind返回後再次傳參的))
    };
  };
})();

 

2.做一個簡單的cache工具

// 閉包隱藏數據,只提供 API
function createCache() {
    const data = {} // 閉包中的數據,被隱藏,不被外界訪問
    return {
        set: function (key, val) {
            data[key] = val
        },
        get: function (key) {
            return data[key]
        }
    }
}

const c = createCache()
c.set('a', 100)
console.log( c.get('a') )

如果不調用set方法,就不能設置鍵值對,如果不調用get方法,就不能存儲鍵值對

比如想要設置data.b = 101會顯示data is not defined,無法訪問data

 

3.創建10個<a>標籤,點擊的時候彈出對應的序號

錯誤示範

let i, a
for (i = 0; i < 10; i++) {
    a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}

for循環很快結束了,i爲10,回調函數裏面引用了外部的i,形成了閉包,回調函數什麼時候執行呢?什麼時候點擊就什麼時候執行。等到點擊的時候for循環早就執行完了,i爲10,所以點擊每個a的時候都是彈出10

let a
for (let i = 0; i < 10; i++) {
    a = document.createElement('a')
    a.innerHTML = i + '<br>'
    a.addEventListener('click', function (e) {
        e.preventDefault()
        alert(i)
    })
    document.body.appendChild(a)
}

還是塊作用域的問題,因爲i現在不是全局的,是在for循環的塊裏面的,回調函數在往上找i變量的時候就會在塊裏面找到,每次循環都是一個塊作用域,自然就對應i的值。

 

關注、留言,我們一起學習。

 

===============Talk is cheap, show me the code================

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章