this指向詳解
六月第一篇文章,也是我第一次接觸思維腦圖,並嘗試將它運用到平時的學習中,我們共勉!
思考 + 導圖 + 示例代碼 => 船新版本
目錄
前言+思考題
記得當時找實習的時候,總是會在簡歷上加上一句——熟悉Js,例如this指向、call、apply等…
而每次投遞簡歷時我都會經歷如下步驟
- 面試前,去問度娘——this指向可以分爲哪幾種啊~、call和apply的區別是什麼?底氣由0% 猛漲到了 50%;
- 面試中,面試官隨便扔上來幾道題,我都可以“堅定的”給出答案,結果總是不盡人意…
- 面試後,我會羞愧的刪除掉簡歷上的這一條。而再之後投遞簡歷時我又再次加上了這一條…
思考題
下面幾道題是我在網上搜索出來的熱度較高的問題,如果大佬們可以輕鬆的回答上,並有清晰的思路,不妨直接點個贊吧(畢竟也消耗了不少腦細胞),如果大佬們能在評論處指點一二,就更好了!!!
填空題:
- 執行Javascript中的
【 】
函數會創建一個新函數,新函數與被調函數具有相同的函數體,當目標函數被調用時 this 值指向第一個參數。
問答題:
- 請你談一下改變函數內部this指針的指向函數有哪幾種,他們的區別是什麼?
- this的指向可以分爲哪幾種?
代碼分析題:
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)()
一、this的指向
百度、谷歌上輸入“this的指向”關鍵字,大幾千條文章肯定是有的,總不至於爲了全方面、無死角的掌握它就要將所有的文章都看一遍吧?所以不如梳理出一個穩固的框架,順着我們的思路來填充它。
思維導圖
本節精華:
- this 總是(非嚴格模式下)指向一個對象,而具體指向哪個對象是在運行時基於函數的
執行環境
動態綁定的,而非函數被聲明時的環境; - 除了不常用的with和eval的情況,具體到實際應用中,this指向大概可以分爲四種:
- 作爲對象的方法調用;
- 作爲普通函數調用;
- 構造器調用;
- call 或 apply調用;
- 箭頭函數中,this指向函數上層作用域的this;
- 構造器和普通函數的區別在於
被調用的方式
; - A,call(B) => 可以理解成在B的作用域內調用了A方法;
分析
1、作爲對象的方法調用
當函數作爲對象的方法被調用時,this指向該對象
var obj = {
a: 'yuguang',
getName: function(){
console.log(this === obj);
console.log(this.a);
}
};
obj.getName(); // true yuguang
2、作爲普通函數調用
當函數不作爲對象的屬性被調用,而是以普通函數的方式,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
3、構造器調用
除了一些內置函數,大部分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}
只要構造器不顯示的返回任何數據,或者返回非對象類型的數據,就不會造成上述問題。
4、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保存在作用域中
二、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
三、模擬實現一個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));
四、bind
bind() 方法創建一個新的函數,在 bind() 被調用時,這個新函數的 this 被指定爲 bind() 的第一個參數,而其餘參數將作爲新函數的參數,供調用時使用 —— MDN
提到了call和apply,就繞不開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版LeetCode題解
- 前端進階筆記
這是我第一次嘗試腦圖+代碼實例相結合的方式來展開知識,如果您看到了最後,不妨收藏、點贊、評論一下吧!!!
持續更新,您的三連就是我最大的動力,虛心接受大佬們的批評和指點,共勉!