this指向詳解,思惟腦圖與代碼的結合,讓你一篇搞懂this、call、apply。系列(一)

this指向詳解

這是我在segmentfault的第一篇文章,歡迎你們指正
思考 + 導圖 + 示例代碼 => 船新版本

目錄

  • 前言+思考題
  • 1、this的指向
  • 2、call和apply
  • 3、模擬實現一個call
  • 4、bind
  • 5、結尾

前言+思考題

記得當時找實習的時候,老是會在簡歷上加上一句——熟悉Js,例如this指向、call、apply等...前端

image

而每次投遞簡歷時我都會經歷以下步驟git

  • 面試前,去問度娘——this指向能夠分爲哪幾種啊~、call和apply的區別是什麼?底氣由0% 猛漲到了 50%;
  • 面試中,面試官隨便扔上來幾道題,我均可以「堅決的」給出答案,結果老是不盡人意...
  • 面試後,我會羞愧的刪除掉簡歷上的這一條。而再以後投遞簡歷時我又再次加上了這一條...

image

思考題

下面幾道題是我在網上搜索出來的熱度較高的問題,若是大佬們能夠輕鬆的回答上,並有清晰的思路,不妨直接點個贊吧(畢竟也消耗了很多腦細胞),若是大佬們能在評論處指點一二,就更好了!!!github

填空題:web

  • 執行Javascript中的【 】函數會建立一個新函數,新函數與被調函數具備相同的函數體,當目標函數被調用時 this 值指向第一個參數。

問答題:面試

  • 請你談一下改變函數內部this指針的指向函數有哪幾種,他們的區別是什麼?
  • this的指向能夠分爲哪幾種?

代碼分析題:segmentfault

var name = 'window'

var person1 = {
  name: 'person1',
  show1: function () {
    console.log(this.name)
  },
  show2: () => console.log(this.name),
  show3: function () {
    return function () {
      console.log(this.name)
    }
  },
  show4: function () {
    return () => console.log(this.name)
  }
}
var person2 = { name: 'person2' }

person1.show1()
person1.show1.call(person2)

person1.show2()
person1.show2.call(person2)

person1.show3()()
person1.show3().call(person2)
person1.show3.call(person2)()

person1.show4()()
person1.show4().call(person2)
person1.show4.call(person2)()

1、this的指向

百度、谷歌上輸入「this的指向」關鍵字,大幾千條文章確定是有的,總不至於爲了全方面、無死角的掌握它就要將全部的文章都看一遍吧?因此不如梳理出一個穩固的框架,順着咱們的思路來填充它。數組

思惟導圖

在這裏插入圖片描述

本節精華:

  • this 老是(非嚴格模式下)指向一個對象,而具體指向哪一個對象是在運行時基於函數的執行環境動態綁定的,而非函數被聲明時的環境;
  • 除了不經常使用的with和eval的狀況,具體到實際應用中,this指向大概能夠分爲四種:瀏覽器

    • 做爲對象的方法調用;
    • 做爲普通函數調用;
    • 構造器調用;
    • call 或 apply調用;
    • 箭頭函數中,this指向函數上層做用域的this;
  • 構造器普通函數的區別在於被調用的方式
  • A,call(B) => 能夠理解成在B的做用域內調用了A方法;

分析

一、做爲對象的方法調用 app

當函數做爲對象的方法被調用時,this指向該對象框架

var obj = {
    a: 'yuguang',
    getName: function(){
        console.log(this === obj);
        console.log(this.a);
    }
};

obj.getName(); // true yuguang

二、做爲普通函數調用

當函數不做爲對象的屬性被調用,而是以普通函數的方式,this老是指向全局對象(在瀏覽器中,一般是Window對象)

window.name = 'yuguang';

var getName = function(){
    console.log(this.name);
};

getName(); // yuguang

或者下面這段迷惑性的代碼:

window.name = '老王'
var obj = {
    name: 'yuguang',
    getName: function(){
        console.log(this.name);
    }
};

var getNew = obj.getName;
getNew(); // 老王

而在ES5的嚴格模式下,this被規定爲不會指向全局對象,而是undefined

三、構造器調用

除了一些內置函數,大部分Js中的函數均可以成爲構造器,它們與普通函數沒什麼不一樣

構造器普通函數的區別在於被調用的方式
當new運算符調用函數時,老是返回一個對象,this一般也指向這個對象

var MyClass = function(){
    this.name = 'yuguang';
}
var obj = new MyClass();
obj.name; // yuguang

可是,若是顯式的返回了一個object對象,那麼這次運算結果最終會返回這個對象。

var MyClass = function () {
    this.name = 1;
    return {
        name: 2
    }
}
var myClass = new MyClass(); 
console.log('myClass:', myClass); // { name: 2}

只要構造器不顯示的返回任何數據,或者返回非對象類型的數據,就不會形成上述問題。

四、call或apply調用

跟普通的函數調用相比,用call和apply能夠動態的改變函數的this

var obj1 = {
    name: 1,
    getName: function (num = '') {
        return this.name + num;
    }
};

var obj2 = {
    name: 2,
};
// 能夠理解成在 obj2的做用域下調用了 obj1.getName()函數
console.log(obj1.getName()); // 1
console.log(obj1.getName.call(obj2, 2)); // 2 + 2 = 4
console.log(obj1.getName.apply(obj2, [2])); // 2 + 2 = 4

5.箭頭函數

箭頭函數不會建立本身的this,它只會從本身的做用域鏈的上一層繼承this。

所以,在下面的代碼中,傳遞給setInterval的函數內的this與封閉函數中的this值相同:

this.val = 2;
var obj = {
    val: 1,
    getVal: () => {
        console.log(this.val);
    }
}

obj.getVal(); // 2

常見的坑

就像標題同樣,有的時候this會指向undefined

狀況一

var obj = {
    name: '1',
    getName: function (params) {
        console.log(this.name)
    }
};
obj.getName();

var getName2 = obj.getName;
getName2();

這個時候,getName2()做爲普通函數被調用時,this指向全局對象——window。

狀況二

當咱們但願本身封裝Dom方法,來精簡代碼時:

var getDomById = function (id) {
    return document.getElementById(id);
};
getDomById('div1') //dom節點

那麼咱們看看這麼寫行不行?

var getDomById = document.getElementById
getDomById('div1') // Uncaught TypeError: Illegal invocation(非法調用)

這是由於:

  • 當咱們去調用document對象的方法時,方法內的this指向document
  • 當咱們用getId應用document內的方法,再以普通函數的方式調用,函數內容的this就指向了全局對象。

利用call和apply修正狀況二

document.getElementById = (function (func) {
    return function(){
        return func.call(document, ...arguments)
    }
})(document.getElementById) 
// 利用當即執行函數將document保存在做用域中

image

2、call和apply

不要由於它的「強大」而對它產生抗拒,瞭解並熟悉它是咱們必需要作的,共勉!

思惟導圖

在這裏插入圖片描述

1.call和apply區別

先來看區別,是由於它們幾乎沒有區別,下文代碼實例call和apply均可以輕易的切換。

當它們被設計出來時要作到的事情一摸同樣,惟一的區別就在於傳參的格式不同

  • apply接受兩個參數

    • 第一個參數指定了函數體內this對象的指向
    • 第二個參數爲一個帶下標的參數集合(能夠是數組或者類數組)
  • call接受的參數不固定

    • 第一個參數指定了函數體內this對象的指向
    • 第二個參數及之後爲函數調用的參數

由於在全部(非箭頭)函數中均可以經過arguments對象在函數中引用函數的參數。此對象包含傳遞給函數的每一個參數,它自己就是一個類數組,咱們apply在實際使用中更常見一些。

call是包裝在apply上面的語法糖,若是咱們明確的知道參數數量,而且但願展現它們,可使用call。

當使用call或者apply的時候,若是咱們傳入的第一個參數爲null,函數體內的this會默認指向宿主對象,在瀏覽器中則是window

借用其餘對象的方法

咱們能夠直接傳null來代替任意對象

Math.max.apply(null, [1, 2, 3, 4, 5])

2.call和apply能作什麼?

使用一個指定的 this 值和單獨給出的一個或多個參數來調用一個函數——來時MDN

  • 調用構造函數來實現繼承;
  • 調用函數而且指定上下文的 this;
  • 調用函數而且不指定第一個參數;

1.調用構造函數來實現繼承

經過「借用」的方式來達到繼承的效果:

function Product(name, price) {
    this.name = name;
    this.price = price;
}

function Food(name, price) {
    Product.call(this, name, price); //
    this.category = food;
}

var hotDog = new Food('hotDog', 20);

2.調用函數而且指定上下文的 this

此時this被指向了obj

function showName() {
    console.log(this.id + ':' + this.name);
};

var obj = {
    id: 1,
    name: 'yuguang'
};

showName.call(obj)

3.使用call單純的調用某個函數

Math.max.apply(null, [1,2,3,10,4,5]); // 10

3、模擬實現一個call

先來看一下call幫咱們須要作什麼?

var foo = {
    value: 1
};
function show() {
    console.log(this.value);
};
show.call(foo); //1

就像解方程,要在已知條件中尋找突破哦口:

  • call 使得this的指向變了,指向了foo;
  • show 函數被執行了;
  • 傳入的參數應爲 this + 參數列表;

初版代碼

上面提到的3點,僅僅完成了一點,且傳入的參數

var foo = {
    value: 1
};
function show() {
    console.log(this.value);
};
Function.prototype.setCall = function (obj) {
    console.log(this); // 此時this指向show
    obj.func = this; // 將函數變成對象的內部屬性
    obj.func(obj.value); // 指定函數
    delete obj.func // 刪除函數,當作什麼都沒發生~
}
show.setCall(foo);

第二版代碼

爲了解決參數的問題,咱們要能獲取到參數,而且正確的傳入:

var foo = {
    value: 1
};
function show(a, b) {
    console.log(this.value);
    console.log(a + b);
};
Function.prototype.setCall = function (obj) {
    obj.fn = this; // 將函數變成對象的內部屬性
    var args = [];
    for(let i = 1; i < arguments.length; i++){
        args.push('arguments[' + i + ']');
    }
    eval('obj.fn(' + args + ')'); // 傳入參數
    delete obj.fn; // 刪除函數,當作什麼都沒發生~
}

show.setCall(foo, 1, 2); // 1 3

此時,咱們就能夠作到,傳入多個參數的狀況下使用call了,可是若是你僅想用某個方法呢?

第三版代碼

Function.prototype.setCall = function (obj) {
    var obj = obj || window;
    obj.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
      }
      var result = eval('obj.fn(' + args +')');
      delete obj.fn;
      return result;
};
// 測試一下
var value = 2;
var obj = { value: 1 };

function bar(name, age) {
      console.log(this.value);
      return {
        value: this.value,
        name: name,
        age: age
      }
}
bar.setCall(null); // 2
console.log(bar.setCall(obj, 'yuguang', 18));

4、bind

bind() 方法建立一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其他參數將做爲新函數的參數,供調用時使用 —— MDN

提到了callapply,就繞不開bind。咱們試着來模擬一個bind方法,以便加深咱們的認識:

Function.prototype.bind = function (obj) {
    var _this = this; // 保存調用bind的函數
    var obj = obj || window; // 肯定被指向的this,若是obj爲空,執行做用域的this就須要頂上嘍
    return function(){
        return _this.apply(obj, arguments); // 修正this的指向
    }
};

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

var func = function(){
    console.log(this.name);
}.bind(obj);

func(); // 1

這樣看上去,返回一個原函數的拷貝,並擁有指定的 this 值,仍是挺靠譜的哦~

寫在最後

JavaScript內功基礎部分第一篇,總結這個系列是受到了冴羽大大的鼓勵和啓發,本系列總章數待定,保證都是咱們在面試最高頻的,但在工做中經常被忽略的。

JavaScript內功系列:

  1. 本文
  2. 下一篇預發:原型和原型鏈

關於我

  • 花名:餘光
  • 前端開發一枚,水平有限,虛心學習中

其餘沉澱

若是您看到了最後,不妨收藏、點贊、評論一下吧!!!
持續更新,您的三連就是我最大的動力,虛心接受大佬們的批評和指點,共勉!

相關文章
相關標籤/搜索