6. JavaScript this指向相關

LemonSea發表於2020-02-11

this 的概念與指向

this 的概念

通常來講,this 的值是在執行的時候才能確認的,定義的時候不能確認。因為 this 是執行上下文環境的一部分,而執行上下文需要在程式碼執行前確定,而不是定義的時候。所以 this 永遠指向最後呼叫它的那個物件。

但,這只是通常來講。apply、call、bind、箭頭函式都會改變 this 的指向。

作為一個函式呼叫

// 情況1
function foo() {
  console.log(this.a) //1
}
var a = 1
foo()  //this -> window
複製程式碼

作為方法呼叫

// 情況2
function fn(){
  console.log(this);
}
var obj={fn:fn};
obj.fn();  //this -> obj
複製程式碼

建構函式中

// 情況3
function CreateJsPerson(name,age){
// this是當前類的一個例項p1
this.name = name;  // => p1.name=name
this.age = age;  // => p1.age=age
}
var p1 = new CreateJsPerson("尹華芝",48);
複製程式碼

call、apply、bind

// 情況4
function add(c, d){
  return this.a + this.b + c + d;
}
var o = {a:1, b:3};
add.call(o, 5, 7);  // 1 + 3 + 5 + 7 = 16  //這裡使用了 call 對 this 進行了重定向
add.apply(o, [10, 20]);  // 1 + 3 + 10 + 20 = 34  //這裡使用了 apply 對 this 進行了重定向
複製程式碼

箭頭函式

// 情況5
<button id="btn1">箭頭函式this</button>
<script type="text/javascript">   
    let btn1 = document.getElementById('btn1');
    let obj = {
        name: 'kobe',
        age: 39,
        getName: function () {
            btn1.onclick = () => {
                console.log(this);//obj
            };
        }
    };
    obj.getName();
</script>
複製程式碼

箭頭函式在自己的作用域內不繫結 this,即沒有自己的 this,如果要使用 this ,就會指向定義時所在的作用域的 this

在 《ES6 標準入門中》:箭頭函式的this,總是指向定義時所在的物件,而不是執行時所在的物件。

由於箭頭函式的 this 是在定義時確定的,所以我們不能在建構函式中使用箭頭函式,建構函式的 this 要指向例項才行,因此只能使用一般的函式。

總結

  • 對於直接呼叫 foo 來說,不管 foo 函式被放在了生命地方,this 一定是 window
  • 對於 obj.foo() 來說,我們要記住,誰呼叫了函式,誰就是 this,所以在這個場景下,foo 函式中的 this 就是 obj 物件
  • 建構函式模式中,類中(函式體中)出現的 this.xxx = xxx 中的 this 就是當前類的一個例項
  • call、apply、bind 的 this 是第一個引數(call 的接收和 apply 不同,apply 接收陣列,而 call 則是用“,”分隔,進行接收)
  • 箭頭函式沒有自己的this,看其外層是否有函式,如果有,外層函式的 this 就是內部箭頭函式的 this,如果沒有,this 就是 window。需要注意的是:箭頭函式的 this 始終指向函式定義時的 this,而非執行時。

81223756

call、apply 與 bind

call、apply、bind 本質都是改變 this 的指向,不同點 call、apply 是直接呼叫函式,bind 是返回一個新的函式。callapply 就只有引數上不同。

call 手寫程式碼

call() 讓函式執行,第一個引數讓 this 的指向改為傳進去的引數,後面的當引數傳進函式裡面。

返回值為原函式的返回值,如果不傳第一個引數為 this 就指向 window。

ES6 版:

Function.prototype.ca112 = function (context, ...arrs ){
    context = context || window; // 因為傳遞過來的 context 很可能是 null
    context.fn = this;  // 讓 fn 的上下文是 context
    const result = context.fn(.. .arrs); 
    delete context.fn; 
    return result; 
}
複製程式碼

ES5 版:

Function.prototype. call2 = function (context) { 
    var context = context || window;  // 因為傳遞過來的 context 很可能是 null
    context.fn = this; 
    var args = [];
    for (var i = 1; i< arguments.length; i++){
        // 不這樣的話,字串的引號會被去掉,變成變數
        args.push("arguments[" + i + "]");
    }
    args = args.join(",");  // 把陣列變成字串
    
    // 相當於執行 context.fn(arguments[1], arguments[2])
    var result = eval("contest.fn(" + args + ")");
    delete context.fn;
    return result;
}
複製程式碼

ES5 版本的使用 eval 來執行語句,這樣會又一定的效能影響,但是這樣做相容性好

因為不知道會輸入多少個,所以這裡直接使用 arguments 來遍歷好了,先把 arguments 轉成陣列,再轉成字串,然後利用 eval 執行程式碼(看見網上說 eval 有安全性問題,不過這裡這樣就夠了。)

apply 手寫程式碼

ES6 版:

Function.prototype.apply2 = function(context, arr) {
    context = context || window;  // 因為傳遞的可能是 null
    context.fn = this;  // 讓 fn 的上下文成為 context
    arr = arr || [];
    const result = context.fn(...arr);
    delete context.fn;
    return result;  // 因為有可能 this 函式會有返回值
};
複製程式碼

ES5 版:

Function.prototype.apply2 = function(context, arr) {
    var context = context || window;
    context.fn = this;
    var args = [];
    var params = arr || [];
    for(var i = 0; i < params.length; i++) {
        args.push("params[" + i + "]");  
    }
    args = args.join(",");
    
    var result = eval("contest.fn(" + args + ")");
    delete context.fn;
    return result;
}
複製程式碼

bind 手寫程式碼

bind 是封裝了 call 的方法改變了 this 的指向並返回一個新的函式

ES6 版:

Function.prototype.bind2 = function(context, ...arrs) {
    let _this = this;
    return function() {
        _this.call(context, ...arrs, ...arguments);
    }
}
複製程式碼

ES5 版:

Function.prototype.bind2 = function(context) {
    var _this = this;
    var argsParent = Array.prototype.slice.call(arguments, 1);
    return function() {
        var args = argsParent.concat(Array.prototype.slice.call(arguments));
        _this.apply(context, args);
    };
}
複製程式碼

三者的同異性總結

call、apply、bind

  1. 三者都是用來改變函式的 this 物件的指向的
  2. 第一個引數都是 this 要指向的物件
  3. 都可以利用後續引數進行傳參
  • 引數傳遞

call 方法傳參是傳一個或多個引數,第一個引數是指定的物件

func.call(thisArg,arg1,arf2,……)
複製程式碼

apply 方法傳參是傳一個或兩個物件,第一個引數是指定的物件,第二個引數是一個陣列或類陣列(說到類陣列就想起了 arguments)

func.apply(thisArg,【argsArray】)
複製程式碼

bind 方法傳參是傳一個或者多個引數,跟 call 方法傳遞引數一樣。

func.bind(this.thisArg,arg1,arg2,arg3……)
複製程式碼
  • 呼叫後是否立即執行

call apply 在函式呼叫它們之後,就會立即執行這個函式;

而函式呼叫了 bind 後,會返回撥用函式的引用,如果要執行的話,需要執行返回函式的引用。

let name = 'window name';
let obj = {
    name: 'call_me_R'
};
function sayName() {
    console.log(this.name);
}

sayName();  // window name
sayName.call(obj);  // call_me_R
sayName.apply(obj);  // call_me_R

let _sayName = sayName.bind(obj);
_syaName();  // call_me_R
複製程式碼

執行的區別在與 ball 和 apply 都是立即執行的,bind 會返回回撥函式,手動執行回撥函式以執行。

new 與 Object.create()

new 做的事情

New 關鍵字會進行如下的操作

  • 建立一個空的簡單 JavaScript 物件(即 {})

  • 連結該物件(即設定該物件的建構函式)到另一個物件

  • 將建立的新的物件作為 this 的上下文

  • 如果該函式沒有返回物件,則返回 this

  • new 會建立一個新的物件,並且這個新物件繼承建構函式的 prototype,也就是說建立的例項的 proto 指向建構函式的 prototype

  • new Object()會建立一個例項,該例項的 proto 指向 Object 的 prototype

手寫 new 四個步驟:

  1. 建立一個空物件,並且 this 變數引用該物件
  2. 繼承函式的原型
  3. 屬性和方法加入 this 引用的物件中,並執行函式
  4. 新建立的物件有 this 所引用,並且最後隱式返回 this
function _newfunc{
    let target = {},
    target.__proto__ = func.prototype;
    let res = func.call(target);
    iftypeof(res)=='object' || typeof(res)=='function'){
        return res;
    }
    return target;
}
複製程式碼

簡單來說:

new 做的三件事情:

  • 指定 prototype
  • 用 call 呼叫物件
  • 返回 this
function myNew (fun{
  return function ({
    // 建立一個新物件且將其隱式原型指向建構函式原型
    let obj = {
      __proto__ : fun.prototype
    }
    // 執行建構函式
    fun.call(obj, ...arguments)
    // 返回該物件
    return obj
  }
}

function person(name, age{
  this.name = name
  this.age = age
}

let obj = myNew(person)('chen'18// {name: "chen", age: 18}
複製程式碼

Object.Create() 基本實現及其原理

// 思路:將傳入的物件作為原型
function create(obj) {
  function F() {}
  F.prototype = obj
  return new F()
}
複製程式碼

Object.Create 是建立了一個新的物件並返回,這個新物件的原型指向了拷貝的物件,當我們通過 b.a 訪問 obj.a 時,是通過原型進行訪問的。

但是要注意的是,Object.Create 並不是深拷貝,Object.Create() 新建的物件共享的是拷貝的物件的引用型別的地址(淺拷貝)。

所以如果修改的是引用型別,還是會變化。

由於 Object.create() 還可以傳遞第二個引數,所以更好的實現是:

function myCreate(proto, properties) {
    // 新物件
    let fn = function() {};
    fn.prototype = proto;
    if(properties) {
        // defineProperties 在新物件上定義新的屬性或修改現有屬性
        Object.defineProperties(fn, properties)
    }
    return new fn();
}
複製程式碼

new 與 object.create 的異同

  • new Object() 繼承內建物件 Object,Object.create 繼承指定物件
  • 可以通過 Object.create(null) 建立一個乾淨的物件,也就是沒有原型,而 new Object() 建立的物件是 Object 的例項,原型永遠指向 Object.prototype

81223759

Object.create 接受兩個引數,即 object.create(proto,propertiesObject)

proto:現有的物件,即新物件的原型物件(新建立的物件 proto 將指向該物件)。如果 proto 為 null,那麼建立出來的物件是一個 {} 並且沒有原型。

81223757

propertiesObject 可選,給新物件新增新屬性以及描述器。如果沒有指定即建立一個 {},有原型也有繼承 Object.prototype 上的方法。可參考 Object.defineProperties()的第二個引數。

81223758

參考

相關文章