跟我左手右手一塊兒慢動做,右手左手慢動做重複。額~貌似哪裏有點不對勁哎?讓我想一想,右手?左手?慢動做??重複???重播???不對不對,是左手call,右手apply,一塊兒來bind this。es6
額,這都能強扯一波,好吧,讓我吐血一波~~~提及了js中的this,確實是個有趣的話題,也是不少小夥伴一開始傻傻分不清的老命題了。算是老梗重提,再來聊聊this吧。api
關於this,首先要提的一點就是,它指向一個對象,具體指向誰是由函數運行時所處的上下文決定的。這是最重要的一個概念,也是理解js中this的關鍵。所謂的上下文,能夠理解爲函數運行時的環境,例如一個函數在全局中運行,那麼它的上下文就是這個全局對象,客戶端中這個global對象就是window
;函數做爲對象的方法運行,那麼它的上下文就是該對象。數組
var name1 = 'hello this';
window.name2 = 'hello global';
function func () {
console.log(this.name1); // 輸出:"hello this"
console.log(this.name2); // 輸出:"hello global"
}
func();複製代碼
這裏的代碼你們天然一眼就知道結果了,結果寫在了上面的註釋裏。經過運行結果咱們知道,普通函數在全局調用中,this指向全局對象。這裏咱們定義了一個全局變量name1,和一個window的屬性name2,因此this.name1和this.name2如咱們的預期指向了這兩個值。值得一提的是:定義的全局變量,是被做爲全局對象window的屬性存在的哦。此時咱們打印看下window對象,看圖:瀏覽器
(2)ES5嚴格模式下的函數調用:this再也不指向全局對象,而是undefined。bash
function strictFunc () {
'use strict'
console.log(this)
console.log(this.name)
}
strictFunc()複製代碼
咱們先看下運行結果:babel
能夠看到,this打印出來的值是undefined
,而this.name
會直接報錯。由此說明,嚴格模式下,this已經再也不指向全局對象,而是undefined
值。引用undefined
和null
值的屬性會報Uncaught TypeError
錯,這點咱們在平常開發中須要注意一下,以避免由於一個錯誤致使後面的程序直接掛掉(這是js單線程的緣由,一旦程序出錯,後面便不會再執行)。特別是咱們在拿到一些不是咱們決定的數據(例如後臺返回的)進行處理的時候,使用對象的屬性時最好判斷一下,這樣在極端狀況下,也能夠保證咱們的程序繼續跑下去,而不至於直接掛掉:app
obj && obj.name
// 而不是直接取值:
obj.name
或者用try/catch捕獲錯誤:
try {
const { data } = await api.getArticleList()
} catch {
} finally {
}
複製代碼
(3)函數做爲對象的方法使用:this指向該對象函數
var obj = {
name: 'xiaoming',
getInfo () {
return '姓名: ' + this.name;
}
}
console.log(obj.getInfo()); // 姓名: xiaoming 複製代碼
當對象當屬性的值是一個函數時,咱們會稱這個函數是這個對象的一個方法。該方法中的this在運行時指向的是該對象。上面的例子的輸出結果也看的清清楚楚,然鵝,沒錯,就是鵝,現實有時候是會啪啪打臉的,打的響亮亮的、輕脆脆的、綠油油的~哎,我爲何要說綠油油,毛病。下面我簡單改寫一個上面的代碼:工具
// 仍是這個obj,仍是熟悉的味道
var obj = {
name: 'xiaoming',
getInfo () {
return '姓名: ' + this.name;
}
}
// 定義一個引入obj.getInfo的變量
var referenceGetInfo = obj.getInfo;
console.log(referenceGetInfo()); // 輸出:姓名:複製代碼
最終咱們沒有拿到預期的name值,打臉了吧,說好了的指向該對象的呢!果真咱們男人都是騙子,都是大豬蹄子!性能
這是爲何呢?咱們知道js分爲兩種數據類型:基本數據類型,如string、number、undefined、null
等,引用類型,如object
。而像數組、函數等,本質都是對象,因此都是引用類型。函數名只不過是指向該函數在內存中位置的一個引用。因此,這裏var referenceGetInfo = obj.getInfo
在賦值以後,referenceGetInfo也只是該函數的一個引用。在看referenceGetInfo
的調用位置,是在全局中,因此是做爲普通函數調用的。由此this
指向window
,因此沒有值。能夠在getInfo
函數中,增長以下驗證,結果必然是true
:
console.log(this === window)複製代碼
(4)構造器函數中的this:指向該構造器返回的新對象
提及構造器函數,可能感受會有些生硬,其實就是咱們常說的定義類時的那個函數。例如,下面這個最多見的一個類(構造器函數):
// 定義Person類
var Person = function (name, sex) {
this.name = name;
this.sex = sex;
}
// 定義Person類的原型對象
Person.prototype = {
constructor: Person,
getName: function () {
return '我叫:' + this.name;
},
getSex: function () {
return '性別:' + this.sex;
}
}
// 實例化一個p1
var p1 = new Person('愣錘', '男');
// 調用p1的方法
console.log(p1.getName()); // 我叫:愣錘
console.log(p1.getSex()); // 性別:男複製代碼
構造器函數本是也是一個函數,若是直接調用該函數,那它和普通函數沒什麼區別。可是經過new
調用以後,那它就成爲了構造器函數。構造器函數在實例化時會返回一個新建立的對象,並將this
指向該對象。因此this.name的值是"愣錘"。另外這裏再提一點,若是你擔憂用戶使用類時忘記加new,能夠經過以下方式,強制使用new調用:
var Person = function (name, sex) {
// 在構造器中增長以下這一行,其他不變
if (!(this instanceof Person)) return new Person(name, sex);
this.name = name;
this.sex = sex;
}複製代碼
該行代碼判斷了當前的this
是不是Person
類的實例,若是不是則強制返回一個經過new
初始化的類。覺得若是用戶忘記使用new
初始化類,那麼此時的構造器函數是做爲普通函數調用的,this
在非嚴格模式下指向window
,確定不會是Person
類的實例,因此咱們直接強制返回new
初始化。這也是咱們在開發類庫時可使用的一個小技巧。
(1)call()方法和apply()方法都是ES3中就存在的方法,能夠改變函數的this指向,二者的功能徹底同樣,因此這裏放在一塊兒說。惟一的區別是二者調用時傳入的參數不一樣,後面會仔細介紹。
// 仍是熟悉的味道,仍是那個obj
var obj = {
name: 'xiaoming',
getInfo (sex) {
return '姓名: ' + this.name + '性別:' + this.sex || '未知';
}
}
// 定義另外一個obj對象
var otherObj = {
name: '狗子你變了,你不再是我認識的那個二狗了!'
}
console.log(obj.getInfo.call(otherObj, '女'));
// 姓名: 狗子你變了,你不再是我認識的那個二狗了!性別:女複製代碼
咱們經過call
把obj.getInfo
方法放在ohterObj
這個對象執行,輸出了ohterObj.name
的值,由此驗證了call能夠函數this的指向。call()方法接收多個參數:
apply()方法和call的功能同樣,只不過傳入的參數不同:
用法很簡單,和call同樣就很少介紹了。可是這裏提到了類數組概念,說一下什麼是類數組,能夠理解爲自己不是數組,可是卻能夠像數組同樣擁有length屬性(例如函數的arguments對象)。咱們沒有確切的辦法判斷一個對象是否是類數組,因此這裏咱們只能使用js中的鴨子類型來判斷。何爲鴨子類型:若是它走起路來像鴨子,叫聲也像鴨子,咱們便認爲它就是鴨子。
鴨子類型是js中很重要的一個概念,由於咱們此時並不真正關心它是否是鴨子,咱們只是想聽到鴨子叫/或者看到鴨子走,即咱們要的只是它擁有鴨子的行爲,至於它是否是鴨子,無所謂呀!!!
因此只要一個對象能擁有數組的行爲,咱們就能夠把它做爲數組使用。下面引入underscore中的類數組判斷方法說明:
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};複製代碼
underscore.js中對類數組的判斷其實也是運用了鴨子類型的思想,即判斷若是該對象擁有length屬性且是number類型,而且length的值大於等於0小於等於一個數組的最大元素個數,那咱們就認定他是數組。
好了,有的稍微扯遠了。
下面繼續apply的實際運用場景,例如柯里化函數:
// 定義一個柯里化函數
var currying = function () {
var arrPro = Array.prototype,
fn = arrPro.shift.call(arguments),
args = arrPro.slice.call(arguments);
return function () {
var _args = arrPro.slice.call(arguments);
return fn.apply(fn, args.concat(_args));
}
}
// 定義一個返回a+b的函數var add = function (a, b) {
return a + b;
}
// 將這個求和函數進行柯里化,使其第一項的值恆爲5
var curryAdd = currying(add, 5);
var res = curryAdd(4);
console.log(res); // 9複製代碼
咱們在開發中apply方法和call方法是用的比較多的,例如這裏柯里化函數。特別是高階函數中,函數做爲值返回的時候,會常用apply這些方法來綁定函數運行時的上下文對象。
咱們再看一個更常見的函數節流吧:
// 去抖函數
function debounce (fn, delay) {
var timer;
return function () {
var args = arguments;
var _this = this;
timer && clearTimeout(timer);
timer = setTimeout(function () {
fn.apply(_this, args);
}, delay);
}
}
// 調用,在瀏覽器窗口滾動的狀況下,debounce裏的函數並不會被頻繁觸發,而是滾動結束500ms後觸發
window.addEventListener('scroll', debounce(function () {
console.log('window scroll');
}, 500), false);複製代碼
咱們在這個去抖函數裏,在返回的函數裏,使用裏定時器,而定時器的第一個參數是一個函數,因此造成裏一個局部的函數做用域。爲了能保證咱們的fn函數中的this的正確指向,咱們經過apply改變它的指向。
所謂去抖函數,在一個函數被頻繁調用的時候,若是這次調用距離上一次的時間小於咱們定下的delay 值,那麼取消本次調用。主要用來防止頻繁觸發的問題,從而提供程序運行性能。注意,上面只是一個函數去抖,真正在提高滾動性能的時候,咱們更多的是會將去抖和節流結合起了使用。此處更多地在於演示apply的運用場景,再也不多作節流去抖方面的說明。
call
方法在v8的實現中,實際上是做爲apply
方法的語法糖,由此,咱們能夠試着使用apply來模擬一個call方法(並不是v8源碼實現):
Function.prototype.call = function () {
var ctx = Array.prototype.shift.apply(arguments);
return this.apply(ctx, arguments);
}複製代碼
咱們知道call
方法,第一個參數是上下文對象,因此咱們的第一件事就是取出參數中的第一個參數ctx
,而後把剩餘的參數使用apply的方式調用。so,就是這樣。
(2)說完了call和apply,下面咱們再說一下ES5引入的新方法:Function.prototype.bind
該方法返回一個新的函數,並將該函數的this綁定到指定的上下文環境。接收多個參數:
用法很簡單,相信你們都會用:
// 仍是那個熟悉的狗子,哦不對,仍是那個熟悉的對象
var obj = {
name: 'xiaoming',
getInfo (sex, hobby) {
return '姓名: ' + this.name + ', 性別:' + (sex || '未知') + hobby;
}
}
// 另一個狗子,呸呸呸!另一個對象
var obj2 = {
name: '我已經不是你認識的狗子了'
}
// 輸出:姓名: 我已經不是你認識的狗子了, 性別:男, 興趣:打球
var newGetInfo = obj.getInfo.bind(obj2, '男');console.log(newGetInfo('打球'));複製代碼
能夠看到,bind()
後返回了一個新函數,並把第一個參數後面的參數傳遞給了obj.getInfo
方法,在運行newGetInfo('打球')
時,又繼續把參數傳遞給了obj.getInfo
方法。是否是發現它自然支持了函數柯里化,是否是感受跟咱們上面的柯里化函數功能同樣?
可是bind方法,是es5引入的,在es3是不支持的。這時候可能會說了,es5已是主流了,你們也都已經大量使用es6及更高的語法,反正又babel等工具幫咱們轉換成es5的。沒錯,可是咱們仍是要了解其實現的,好比寫一個bind方法的profill。作到知其然,知其因此然。
// 若是自己支持bind方法,則使用原生的bind方法,不然咱們就實現一個使用
Function.prototype.bind = Function.prototype.bind || function () {
var fn = this; var ctx = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
return function () {
var _args = Array.prototype.slice.call(arguments);
return fn.apply(ctx, args.concat(_args));
}
}複製代碼
講到這,相信已經能夠將this/call/apply方法搞清楚了。由此還引伸出更多的函數節流/去抖/柯里化/反柯里化,仍是能夠繼續深刻深究一下的。