JS基礎(chǔ)精講之【頭疼的this】
頭疼的this????為什么說是頭疼的this,對于常年使用C ,C#,Java等這些面向?qū)ο笳Z言的程序員來說,幾乎天天都和this打交道。在這些語言里,this含義非常明確,就是指向當(dāng)前的對象實例,我們用起來也是相當(dāng)?shù)姆判?。然而,到了JavaScript這個動態(tài)語言里,this的寫法沒變,但是其含義卻大大地不同了,JavaScript中的this總是讓人迷惑,應(yīng)該是js眾所周知的坑之一。個人也覺得js中的this不是一個好的設(shè)計,由于this晚綁定的特性,它可以是全局對象,當(dāng)前對象,或者…有人甚至因為坑大而不用this,我們一般在面試中面試官又會扔過來一堆各種各樣的考察this的題目,很是頭疼。那到底什么是this?this是什么???? this是指包含它的函數(shù)作為方法被調(diào)用時所屬的對象。這句話理解起來感覺還是很拗口的,但是如果你把它拆分開來變成這三句話后就好理解一點了。
- 1.包含它的函數(shù)
- 2.作為方法被調(diào)用時
- 3.所屬的對象
- this 總是(非嚴(yán)格模式下)指向一個對象,而具體指向哪個對象是在運行時基于函數(shù)的
執(zhí)行環(huán)境
動態(tài)綁定的,而非函數(shù)被聲明時的環(huán)境; - 除了不常用的with和eval的情況,具體到實際應(yīng)用中,this指向大概可以分為四種:
- 作為對象的方法調(diào)用;
- 作為普通函數(shù)調(diào)用;
- 構(gòu)造器調(diào)用;
- call 或 apply調(diào)用;
- 箭頭函數(shù)中,this指向函數(shù)上層作用域的this;
- 構(gòu)造器和普通函數(shù)的區(qū)別在于
被調(diào)用的方式
; - A,call(B) => 可以理解成在B的作用域內(nèi)調(diào)用了A方法;
1.1 作為對象的方法調(diào)用
當(dāng)函數(shù)作為對象的方法被調(diào)用時,this指向該對象
var obj = {
a: 'jianxi',
getName: function(){
console.log(this === obj);
console.log(this.a);
}
};
obj.getName(); // true jainxi
1.2?作為普通函數(shù)被調(diào)用當(dāng)函數(shù)不作為對象的屬性被調(diào)用,而是以普通函數(shù)的方式,this總是指向全局對象(在瀏覽器中,通常是Window對象)
window.name = 'jianxi';
var getName = function(){
console.log(this.name);
};
getName(); // jianxi
或者下面這段迷惑性的代碼window.name = 'koa'
var obj = {
name: 'jianxi',
getName: function(){
console.log(this.name)
}
};
var getNew = obj.getName;
getNew(); // koa
而在ES5的嚴(yán)格模式下,this被規(guī)定為不會指向全局對象,而是undefined
1.3 構(gòu)造器調(diào)用
除了一些內(nèi)置函數(shù),大部分Js中的函數(shù)都可以成為構(gòu)造器,它們與普通函數(shù)沒什么不同構(gòu)造器和普通函數(shù)的區(qū)別在于被調(diào)用的方式
:當(dāng)new運算符調(diào)用函數(shù)時,總是返回一個對象,this通常也指向這個對象var MyClass = function(){
this.name = 'jianxi';
}
var obj = new MyClass()
obj.name; // jianxi
但是,如果顯式的返回了一個object對象,那么此次運算結(jié)果最終會返回這個對象。var MyClass = function () {
this.name = 1;
return {
name: 2
}
}
var myClass = new MyClass();
console.log('myClass.name:', myClass.name); // { name: 2}
只要構(gòu)造器不顯示的返回任何數(shù)據(jù),或者返回非對象類型的數(shù)據(jù),就不會造成上述問題。1.4 call或apply調(diào)用
跟普通的函數(shù)調(diào)用相比,用call和apply可以動態(tài)的改變函數(shù)的thisvar obj1 = {
name: 1,
getName: function (num = '') {
return this.name num;
}
};
var obj2 = {
name: 2,
}
// 可以理解成在 obj2的作用域下調(diào)用了 obj1.getName()函數(shù)
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
1.5箭頭函數(shù)箭頭函數(shù)不會創(chuàng)建自己的this,它只會從自己的作用域鏈的上一層繼承this。因此,在下面的代碼中,傳遞給setInterval的函數(shù)內(nèi)的this與封閉函數(shù)中的this值相同:this.name = 2
var obj = {
name: '1',
getName: () => {
console.log(this.name)
}
}
obj.getName()
1.6常見的坑就像標(biāo)題一樣,有的時候this
會指向undefined情況一var obj = {
name: '1',
getName: function (params) {
console.log(this.name)
}
};
obj.getName();
var getName2 = obj.getName;
getName2()
這個時候,getName2()作為普通函數(shù)被調(diào)用時,this指向全局對象——window。情況二當(dāng)我們希望自己封裝Dom方法,來精簡代碼時:
var getDomById = function (id) {
return document.getElementById(id);
};
getDomById('div1') //dom節(jié)點
那么我們看看這么寫行不行?var getDomById = document.getElementById
getDomById('div1') // Uncaught TypeError: Illegal invocation(非法調(diào)用)
這是因為:- 當(dāng)我們?nèi)フ{(diào)用
document
對象的方法時,方法內(nèi)的this指向document
。 - 當(dāng)我們用getId應(yīng)用document內(nèi)的方法,再以普通函數(shù)的方式調(diào)用,函數(shù)內(nèi)容的this就指向了全局對象。
document.getElementById = (function (func) {
return function(){
return func.call(document, ...arguments)
}
})(document.getElementById)
// 利用立即執(zhí)行函數(shù)將document保存在作用域中
二、call和apply
不要因為它的“強(qiáng)大”而對它產(chǎn)生抗拒,了解并熟悉它是我們必須要做的,共勉!1.call和apply區(qū)別
先來看區(qū)別,是因為它們幾乎沒有區(qū)別,下文代碼實例call和apply都可以輕易的切換。當(dāng)它們被設(shè)計出來時要做到的事情一摸一樣,唯一的區(qū)別就在于傳參的格式不一樣
- apply接受兩個參數(shù)
- 第一個參數(shù)指定了函數(shù)體內(nèi)this對象的指向
- 第二個參數(shù)為一個帶下標(biāo)的參數(shù)集合(可以是數(shù)組或者類數(shù)組)
- call接受的參數(shù)不固定
- 第一個參數(shù)指定了函數(shù)體內(nèi)this對象的指向
- 第二個參數(shù)及以后為函數(shù)調(diào)用的參數(shù)
arguments
對象在函數(shù)中引用函數(shù)的參數(shù)。此對象包含傳遞給函數(shù)的每個參數(shù),它本身就是一個類數(shù)組,我們apply在實際使用中更常見一些。call是包裝在apply上面的語法糖,如果我們明確的知道參數(shù)數(shù)量,并且希望展示它們,可以使用call。當(dāng)使用call或者apply的時候,如果我們傳入的第一個參數(shù)為null,函數(shù)體內(nèi)的this會默認(rèn)指向宿主對象,在瀏覽器中則是window
。借用其他對象的方法我們可以直接傳null來代替任意對象Math.max.apply(null, [1, 2, 3, 4, 5])
2.call和apply能做什么?
使用一個指定的 this 值和單獨給出的一個或多個參數(shù)來調(diào)用一個函數(shù)——來時MDN(opens new window)- 調(diào)用構(gòu)造函數(shù)來
實現(xiàn)繼承
; - 調(diào)用函數(shù)并且指定上下文的?
this
; - 調(diào)用函數(shù)并且不指定第一個參數(shù);
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.調(diào)用函數(shù)并且指定上下文的?this
此時this被指向了objfunction showName() {
console.log(this.id ':' this.name);
};
var obj = {
id: 1,
name: 'jianxi'
};
showName.call(obj)
3.使用call單純的調(diào)用某個函數(shù)Math.max.apply(null, [1,2,3,10,4,5]); // 10
三、模擬實現(xiàn)一個call
先來看一下call幫我們需要做什么?var foo = {
value: 1
};
function show() {
console.log(this.value);
};
show.call(foo); //1
就像解方程,要在已知條件中尋找突破哦口:call
?使得this的指向變了,指向了foo;show
?函數(shù)被執(zhí)行了;- 傳入的參數(shù)應(yīng)為?
this
? 參數(shù)列表;
var foo = {
value: 1
}
function show() {
console.log(this.value);
}
Function.prototype.setCall = function (obj) {
console.log(this); // 此時this指向show
obj.func = this; // 將函數(shù)變成對象的內(nèi)部屬性
obj.func(obj.value) // 指定函數(shù)
delete obj.func // 刪除函數(shù),當(dāng)做什么都沒發(fā)生~
}
show.setCall(foo)
第二版代碼為了解決參數(shù)的問題,我們要能獲取到參數(shù),并且正確的傳入:var foo = {
value: 1
}
function show(a, b) {
console.log(this.value);
console.log(a b)
}
Function.prototype.setCall = function (obj) {
obj.fn = this; // 將函數(shù)變成對象的內(nèi)部屬性
var args = [];
for(let i = 1; i < arguments.length; i ){
args.push('arguments[' i ']');
}
eval('obj.fn(' args ')'); // 傳入?yún)?shù)
delete obj.fn; // 刪除函數(shù),當(dāng)做什么都沒發(fā)生~
}
show.setCall(foo, 1, 2); // 1 3
此時,我們就可以做到,傳入多個參數(shù)的情況下使用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, 'kevin', 18));
三、bind
提到了call和apply,就繞不開bind(),來看一下MDN上對**bind()**的解釋:bind() 方法創(chuàng)建一個新的函數(shù),在 bind() 被調(diào)用時,這個新函數(shù)的 this 被指定為 bind() 的第一個參數(shù),而其余參數(shù)將作為新函數(shù)的參數(shù),供調(diào)用時使用。bind 可以被new , 可以進(jìn)行構(gòu)造函數(shù)我們用Js來模擬一個bind方法,以便加深我們的認(rèn)識Function.prototype.newBind = function(context) {
if(typeof this !== 'function') {
throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1); // 間接調(diào)用數(shù)組方法,獲取第一次傳的參數(shù)
let tempFn = function {}; // 利用一個空函數(shù)作為中轉(zhuǎn)
tempFn.prototype = this.prototype; // 修改返回函數(shù)的 prototype 為綁定函數(shù)的 prototype,實例就可以繼承綁定函數(shù)的原型中的值
var resultFn = function () {
var innerArgs = Array.prototype.slice.call(arguments);
if (this instanceof tempFn) { // 如果 返回函數(shù)被當(dāng)做構(gòu)造函數(shù)后,生成的對象是 tempFn 的實例,此時應(yīng)該將 this 的指向指向創(chuàng)建的實例。
return self.apply(this, args.concat(innerArgs));
} else {
return self.apply(context, args.concat(innerArgs))
}
}
resultFn = new tempFn();
return resultFn;
}
這樣看上去,bind總會幫我們返回同樣的
this
值,還是挺堅挺的哦~如果您看到了最后,不妨收藏、點贊、評論一下吧?。?!持續(xù)更新,您的三連就是我最大的動力,共勉!