【前端面試】做用域和閉包

1. 題目

說一下對變量提高的理解javascript

說明this的幾種不一樣使用場景java

建立10個a標籤,點擊的時候彈出來相應的序號數組

如何理解做用域瀏覽器

實際開發中閉包的應用緩存

手動實現call apply bind閉包

2. 知識點

2.1 執行上下文

範圍:一段script或者一個函數app

全局:變量定義、函數聲明 script函數

函數:變量定義、函數聲明、this、arguments (執行以前)性能

函數聲明和函數表達式的區別:this

a(); //報錯  函數表達式 變量聲明 會提早。
var a = function(){}

b(); // 不報錯  函數聲明
function b(){}

變量定義時會默認把他的變量聲明提高:(僅限於他的執行上下文,好比一段script和一個函數中)

console.log(a);
var a = 0;

其實是

var a;
console.log(a);
a = 0;

2.2 this

this要在執行時才能確認,定義時沒法確認。

var a = {
            name:'a',
            fn:function(){
                console.log(this.name);
            }
        }

        a.fn();  // a
        a.fn.apply({name:'b'});  // b  a.fn.call({name:'b'});
        var fn1 = a.fn();
        fn1();  // undefined

this的使用場景

構造函數中(指向構造的對象)

function Fun(name){
        this.name = name;
    }
    var f = new Fun('a');
    console.log(f.name);

對象屬性中(指向該對象)

普通函數中(指向window)

call apply bind

都是用來改變一個函數的this指向,用法略有不一樣。

call:後面的參數爲調用函數的參數列表

function greet(name) {
  console.log(this.animal,name);
}

var obj = {
  animal: 'cats'
};

greet.call(obj,'貓咪');

apply:第二個參數爲調用函數的參數數組

function greet(name) {
  console.log(this.animal,name);
}

var obj = {
  animal: 'cats'
};

greet.apply(obj,['貓咪']);

bind:當綁定函數被調用時,bind傳入的參數會被插入到目標函數的參數列表的開始位置,傳遞給綁定函數的參數會跟在它們後面。

var fun = function (name1,name2){
    console.log(this);
    console.log(name);
}.bind({a:1},"name1");
    fun("name2");

arguments中的this:

var length = 10;
function fn(){
    alert(this.length)
}
var obj = {
    length: 5,
    method: function(fn) {
        arguments[0]()
    }
}

obj.method(fn)//輸出1
這裏沒有輸出5,也沒有輸出10,反而輸出了1,有趣。這裏arguments是javascript的一個內置對象(能夠參見mdn:arguments - JavaScript),是一個類數組(就是長的比較像數組,可是欠缺一些數組的方法,能夠用slice.call轉換,具體參見上面的連接),其存儲的是函數的參數。也就是說,這裏arguments[0]指代的就是你method函數的第一個參數:fn,因此arguments[0]()的意思就是:fn()。

不過這裏有個疑問,爲什麼這裏沒有輸出5呢?我method裏面用this,不該該指向obj麼,至少也會輸出10呀,這個1是鬧哪樣?

實際上,這個1就是arguments.length,也就是本函數參數的個數。爲啥這裏的this指向了arguments呢?由於在Javascript裏,數組只不過使用數字作屬性名的方法,也就是說:arguments[0]()的意思,和arguments.0()的意思差很少(固然這麼寫是不容許的),你更能夠這麼理解:

arguments = {
    0: fn, //也就是 functon() {alert(this.length)} 
    1: 第二個參數, //沒有 
    2: 第三個參數, //沒有
    ..., 
    length: 1 //只有一個參數
}

因此這裏alert出來的結果是1。

若是要輸出5應該咋寫呢?直接 method: fn 就好了。

2.3 做用域

沒有塊級做用域

if(true){
            var name = "test"
        }
        console.log(name);

儘可能不要在塊中聲明變量。

只有函數級做用域

2.4 做用域鏈

自由變量 當前做用域沒有定義的變量 即爲自由變量。

自由變量會去其父級做用域找。是定義時的父級做用域,而不是執行。

var a = 100;
        function f1(){
            var b = 200;
            function f2(){
                var c = 300;
                console.log(a); //自由變量
                console.log(b); //自由變量
                console.log(c);
            }
            f2();
        };
        f1();

2.5 閉包

 一個函數中嵌套另一個函數,而且將這個函數return出去,而後將這個return出來的函數保存到了一個變量中,那麼就建立了一個閉包。

閉包的兩個使用場景

1.函數做爲返回值

function fun(){
            var a = 0;
            return function(){
                console.log(a); //自由變量,去定義時的父級做用域找
            }
        }

        var f1 = fun();
        a = 1000;
        f1();

2.函數做爲參數

function fun(){
            var a = 0;
            return function(){
                console.log(a); //自由變量,去定義時的父級做用域找
            }
        }

        function fun2(f2){
            a = 10000
            f2();
        }

        var f1 = fun();

        fun2(f1);

具體解釋看 高級-閉包中的說明

閉包的兩個做用:

可以讀取其餘函數內部變量的函數

可讓函數內部的變量一直保存在內存中

實際應用場景1:

閉包能夠將一些不但願暴露在全局的變量封裝成「私有變量」。

假若有一個計算乘積的函數,mult函數接收一些number類型的參數,並返回乘積結果。爲了提升函數性能,咱們增長緩存機制,將以前計算過的結果緩存起來,下次遇到一樣的參數,就能夠直接返回結果,而不須要參與運算。這裏,存放緩存結果的變量不須要暴露給外界,而且須要在函數運行結束後,仍然保存,因此能夠採用閉包。

上代碼:

function calculate(param){
    var cache = {};
    return function(){
        if(!cache.parame){
            return cache.param;
        }else{
            //緩存計算....
            //cache.param = result
            //下次訪問直接取
        }
    }
}

實際應用場景2

延續局部變量的壽命

img 對象常常用於進行數據上報,以下所示:

var report = function( src ){
    var img = new Image();
    img.src = src;
};
report( 'http://xxx.com/getUserInfo' );

可是經過查詢後臺的記錄咱們得知,由於一些低版本瀏覽器的實現存在 bug,在這些瀏覽器
下使用 report 函數進行數據上報會丟失 30%左右的數據,也就是說, report 函數並非每一次
都成功發起了 HTTP 請求。

丟失數據的緣由是 img 是 report 函數中的局部變量,當 report 函數的
調用結束後, img 局部變量隨即被銷燬,而此時或許還沒來得及發出 HTTP 請求,因此這次請求
就會丟失掉。

如今咱們把 img 變量用閉包封閉起來,便能解決請求丟失的問題:

var report = (function(){
    var imgs = [];
    return function( src ){
        var img = new Image();
        imgs.push( img );
        img.src = src;
    }
})();

閉包缺點:浪費資源!

3. 題目解答

3.1 說一下對變量提高的理解

變量定義和函數聲明

注意函數聲明和函數表達式的區別

變量定義時會默認把他的變量聲明提高:(僅限於他的執行上下文,好比一段script和一個函數中)

console.log(a);
var a = 0;

其實是

var a;
console.log(a);
a = 0;

3.2 說明this的幾種不一樣使用場景

  • 構造函數中(指向構造的對象)
  • 對象屬性中(指向該對象)
  • 普通函數中(指向window)
  • call apply bind

3.3 建立10個a標籤,點擊的時候彈出來相應的序號

實現方法1:用let聲明i

var body = document.body;
        console.log(body);
        for (let i = 0; i < 10; i++) {
            let obj = document.createElement('i');
            obj.innerHTML = i + '<br>';
            body.appendChild(obj);
            obj.addEventListener('click',function(){
                alert(i);
            })
        }

實現方法2 包裝做用域

var body = document.body;
    console.log(body);
    for (var i = 0; i < 10; i++) {
        (function (i) {
            var obj = document.createElement('i');
            obj.innerHTML = i + '<br>';
            body.appendChild(obj);
            obj.addEventListener('click', function () {
                alert(i);
            })
        })(i)
    }

3.4 實際開發中閉包的應用

可以讀取其餘函數內部變量的函數

可讓函數內部的變量一直保存在內存中

封裝變量,權限收斂

應用1

var report = (function(){
    var imgs = [];
    return function( src ){
        var img = new Image();
        imgs.push( img );
        img.src = src;
    }
})();

用於防止變量銷燬。

應用2

function isFirstLoad() {
        var arr = [];
        return function (str) {
            if (arr.indexOf(str) >= 0) {
                console.log(false);
            } else {
                arr.push(str);
                console.log(true);
            }
        }
    }

    var fun = isFirstLoad();
    fun(10);
    fun(10);

將arr封裝在函數內部,禁止隨意修改,防止變量銷燬。

3.5 手動實現call apply bind

  1. context 爲可選參數,若是不傳的話默認上下文爲 window;
  2. context 建立一個 Symbol 屬性,調用後即刪除,不會影響context
Function.prototype.myCall = function (context) {
      if (typeof this !== 'function') {
        return undefined; // 用於防止 Function.prototype.myCall() 直接調用
      }
      context = context || window;
      const fn = Symbol();
      context[fn] = this;
      const args = [...arguments].slice(1);
      const result = context[fn](...args);
      delete context[fn];
      return result;
    }

apply實現相似call,參數爲數組

Function.prototype.myApply = function (context) {
      if (typeof this !== 'function') {
        return undefined; // 用於防止 Function.prototype.myCall() 直接調用
      }
      context = context || window;
      const fn = Symbol();
      context[fn] = this;
      let result;
      if (arguments[1] instanceof Array) {
        result = context[fn](...arguments[1]);
      } else {
        result = context[fn]();
      }
      delete context[fn];
      return result;
    }

1.判斷是否爲構造函數調用

2.注意參數要插入到目標函數的開始位置

Function.prototype.myBind = function (context) {
      if (typeof this !== 'function') {
        throw new TypeError('Error')
      }
      const _this = this
      const args = [...arguments].slice(1)
      return function F() {
        // 判斷是否用於構造函數
        if (this instanceof F) {
          return new _this(...args, ...arguments)
        }
        return _this.apply(context, args.concat(...arguments))
      }
    }
相關文章
相關標籤/搜索