搜尋
首頁web前端js教程前端基礎進階(五):全方位解讀this

前端基礎進階(五):全方位解讀this

Feb 17, 2017 pm 01:47 PM
前端基礎進階

前端基礎進階(五):全方位解讀this

我們在學習JavaScript的過程中,由於對一些概念理解得不是很清楚,但是又想要透過一些方式把它記下來,於是就很容易草率的給這些概念定下一些方便自己記憶的有偏差的結論。

危害比較大的是,有的不準確的結論在網路上還廣為流傳。

例如對於this指向的理解中,有這樣一種說法:誰調用它,this就指向誰。在我剛開始學習this的時候,我是非常相信這句話的。因為在某些情況下,這樣理解也還算說得通。可是我常常會在開發中遇到一些不一樣的情況,一個由於this的錯誤調用,可以讓我懵逼一整天。那時候我也查資料,在群組裡問大神,可是我仍然搞不清楚「我特麼到底錯哪裡了」。其實只是因為我心中有一個不太準確的結論。

這裡吐槽一下百度搜索,搜索出來的文章,好多知識點都是錯的,害了勞資好久

所以,我認為需要有這樣一篇文章,來幫助大家全方位的解讀this。讓大家對this,有一個正確的,全面的認知。

在這之前,我們需要來回顧一下執行上下文。

在前面幾篇文章中,我有好幾個地方都提到執行上下文的生命週期,為了防止大家沒有記住,再次來回顧一下,如下圖。

前端基礎進階(五):全方位解讀this

在執行上下文的建立階段,會分別產生變數對象,建立作用域鏈,決定this指向。其中變數物件與作用域鏈我們都已經仔細總結過了,而這裡的關鍵,就是確定this指向。

在這裡,我們需要得出一個非常重要一定要牢記於心的結論,this的指向,是在函數被呼叫的時候確定的。也就是執行上下文被創建時確定的。因此我們可以很容易就能理解到,一個函數中的this指向,可以是非常靈活的。例如下面的例子中,同一個函數由於呼叫方式的不同,this指向了不一樣的物件。

var a = 10;
var obj = {
    a: 20
}

function fn () {
    console.log(this.a);
}

fn(); // 10
fn.call(obj); // 20

除此之外,在函數執行過程中,this一旦被確定,就不可更改了。

var a = 10;
var obj = {
    a: 20
}

function fn () {
    this = obj; // 这句话试图修改this,运行后会报错
    console.log(this.a);
}

fn();


 一、全域物件中的this

關於全局物件的this,我之前在存在變數物件的時候提到過,它是一個比較特殊的基礎全域環境中的this,指向它本身。因此,這也相對簡單,沒有那麼多複雜的情況需要考慮。

// 通过this绑定到全局对象
this.a2 = 20;

// 通过声明绑定到变量对象,但在全局环境中,变量对象就是它自身
var a1 = 10;

// 仅仅只有赋值操作,标识符会隐式绑定到全局对象
a3 = 30;

// 输出结果会全部符合预期
console.log(a1);
console.log(a2);
console.log(a3);


二、函數中的this

在總結函數中指向this之前,我想我們有必要透過一些奇怪的例子,來摸一下。

// demo01
var a = 20;
function fn() {
    console.log(this.a);
}
fn();
// demo02
var a = 20;
function fn() {
    function foo() {
        console.log(this.a);
    }
    foo();
}
fn();
// demo03
var a = 20;
var obj = {
    a: 10,
    c: this.a + 20,
    fn: function () {
        return this.a;
    }
}

console.log(obj.c);
console.log(obj.fn());

這幾個例子需要讀者老爺們花點時間稍微感受一下,如果你暫時沒想明白怎麼回事,也不用著急,我們一點一點來分析。

分析之前,我們先直接了當拋出結論。

在一個函數上下文中,this由呼叫者提供,由呼叫函數的方式決定。如果呼叫者函數,被某一個物件所擁有,那麼函數在呼叫時,內部的this指向該物件。如果函數獨立調用,那麼該函數內部的this,則指向undefined。但是在非嚴格模式中,當this指向undefined時,它會被自動指向全域物件。

從結論中我們可以看出,想要準確確定this指向,找到函數的調用者以及區分他是否是獨立調用就變得十分關鍵。

// 为了能够准确判断,我们在函数内部使用严格模式,因为非严格模式会自动指向全局
function fn() {
    'use strict';
    console.log(this);
}

fn();  // fn是调用者,独立调用
window.fn();  // fn是调用者,被window所拥有

在上面的簡單例子中,fn()作為獨立呼叫者,依照定義的理解,它內部的this指向就為undefined。而window.fn()則因為fn被window所擁有,內部的this就指向了window對象。

那麼掌握了這個規則,現在回過頭去看看上面的三個例子,透過添加/去除嚴格模式,那麼你就會發現,原來this已經變得不那麼虛無縹緲,已經有跡可循了。

但是我們要特別注意的是demo03。在demo03中,物件obj中的c屬性使用this.a + 20來計算,而他的呼叫者obj.c並非是一個函數。因此他不適用於上面的規則,我們要對這種方式單獨下一個結論。

當obj在全局聲明時,無論obj.c在什麼地方調用,這裡的this都指向全局對象,而當obj在函數環境中聲明時,這個this指向undefined,在非嚴格模式下,會自動轉向全局對象。可運行下面的範例查看差異。

'use strict';
var a = 20;
function foo () {
    var a = 1;
    var obj = {
        a: 10, 
        c: this.a + 20,
        fn: function () {
            return this.a;
        }
    }
    return obj.c;

}
console.log(foo()); // 运行会报错

實際開發中,並不建議這樣使用this;

上面多次提到的严格模式,需要大家认真对待,因为在实际开发中,现在基本已经全部采用严格模式了,而最新的ES6,也是默认支持严格模式。

再来看一些容易理解错误的例子,加深一下对调用者与是否独立运行的理解。

var a = 20;
var foo = {
    a: 10,
    getA: function () {
        return this.a;
    }
}
console.log(foo.getA()); // 10

var test = foo.getA;
console.log(test());  // 20

foo.getA()中,getA是调用者,他不是独立调用,被对象foo所拥有,因此它的this指向了foo。而test()作为调用者,尽管他与foo.getA的引用相同,但是它是独立调用的,因此this指向undefined,在非严格模式,自动转向全局window。

稍微修改一下代码,大家自行理解。

var a = 20;
function getA() {
    return this.a;
}
var foo = {
    a: 10,
    getA: getA
}
console.log(foo.getA());  // 10

灵机一动,再来一个。如下例子。

function foo() {
    console.log(this.a)
}

function active(fn) {
    fn(); // 真实调用者,为独立调用
}

var a = 20;
var obj = {
    a: 10,
    getA: foo
}

active(obj.getA);


三、使用call,apply显示指定this

JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有着两个方法。它们除了参数略有不同,其功能完全一样。它们的第一个参数都为this将要指向的对象。

如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。这就是call/apply的用法。

function fn() {
    console.log(this.a);
}
var obj = {
    a: 20
}

fn.call(obj);

而call与applay后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同。

function fn(num1, num2) {
    console.log(this.a + num1 + num2);
}
var obj = {
    a: 20
}

fn.call(obj, 100, 10); // 130
fn.apply(obj, [20, 10]); // 50

因为call/apply的存在,这让JavaScript变得十分灵活。因此就让call/apply拥有了很多有用处的场景。简单总结几点,也欢迎大家补充。

1.将类数组对象转换为数组

function exam(a, b, c, d, e) {

    // 先看看函数的自带属性 arguments 什么是样子的
    console.log(arguments);

    // 使用call/apply将arguments转换为数组, 返回结果为数组,arguments自身不会改变
    var arg = [].slice.call(arguments);

    console.log(arg);
}

exam(2, 8, 9, 10, 3);

// result: 
// { '0': 2, '1': 8, '2': 9, '3': 10, '4': 3 }
// [ 2, 8, 9, 10, 3 ]
// 
// 也常常使用该方法将DOM中的nodelist转换为数组
// [].slice.call( document.getElementsByTagName('li') );

2.根据自己的需要灵活修改this指向

var foo = {
    name: 'joker',
    showName: function() {
      console.log(this.name);
    }
}
var bar = {
    name: 'rose'
}
foo.showName.call(bar);

3.实现继承

// 定义父级的构造函数
var Person = function(name, age) {
    this.name = name;
    this.age  = age;
    this.gender = ['man', 'woman'];
}

// 定义子类的构造函数
var Student = function(name, age, high) {

    // use call
    Person.call(this, name, age);
    this.high = high;
}
Student.prototype.message = function() {
    console.log('name:'+this.name+', age:'+this.age+', high:'+this.high+', gender:'+this.gender[0]+';');
}

new Student('xiaom', 12, '150cm').message();

// result
// ----------
// name:xiaom, age:12, high:150cm, gender:man;

简单给有面向对象基础的朋友解释一下。在Student的构造函数中,借助call方法,将父级的构造函数执行了一次,相当于将Person中的代码,在Sudent中复制了一份,其中的this指向为从Student中new出来的实例对象。call方法保证了this的指向正确,因此就相当于实现了基层。Student的构造函数等同于下。

var Student = function(name, age, high) {
    this.name = name;
    this.age  = age;
    this.gender = ['man', 'woman'];
    // Person.call(this, name, age); 这一句话,相当于上面三句话,因此实现了继承
    this.high = high;
}

4.在向其他执行上下文的传递中,确保this的指向保持不变

如下面的例子中,我们期待的是getA被obj调用时,this指向obj,但是由于匿名函数的存在导致了this指向的丢失,在这个匿名函数中this指向了全局,因此我们需要想一些办法找回正确的this指向。

var obj = {
    a: 20,
    getA: function() {
        setTimeout(function() {
            console.log(this.a)
        }, 1000)
    }
}

obj.getA();

常规的解决办法很简单,就是使用一个变量,将this的引用保存起来。我们常常会用到这方法,但是我们也要借助上面讲到过的知识,来判断this是否在传递中被修改了,如果没有被修改,就没有必要这样使用了。

var obj = {
    a: 20,
    getA: function() {
        var self = this;
        setTimeout(function() {
            console.log(self.a)
        }, 1000)
    }
}

另外就是借助闭包与apply方法,封装一个bind方法。

function bind(fn, obj) {
    return function() {
        return fn.apply(obj, arguments);
    }
}

var obj = {
    a: 20,
    getA: function() {
        setTimeout(bind(function() {
            console.log(this.a)
        }, this), 1000)
    }
}

obj.getA();

当然,也可以使用ES5中已经自带的bind方法。它与我上面封装的bind方法是一样的效果。

var obj = {
    a: 20,
    getA: function() {
        setTimeout(function() {
            console.log(this.a)
        }.bind(this), 1000)
    }
}


四、构造函数与原型方法上的this

在封装对象的时候,我们几乎都会用到this,但是,只有少数人搞明白了在这个过程中的this指向,就算我们理解了原型,也不一定理解了this。所以这一部分,我认为将会为这篇文章最重要最核心的部分。理解了这里,将会对你学习JS面向对象产生巨大的帮助。

结合下面的例子,我在例子抛出几个问题大家思考一下。

function Person(name, age) {

    // 这里的this指向了谁?
    this.name = name;
    this.age = age;   
}

Person.prototype.getName = function() {

    // 这里的this又指向了谁?
    return this.name;
}

// 上面的2个this,是同一个吗,他们是否指向了原型对象?

var p1 = new Person('Nick', 20);
p1.getName();

我们已经知道,this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。

通过new操作符调用构造函数,会经历以下4个阶段。

1.创建一个新的对象;

2.将构造函数的this指向这个新对象;

3.指向构造函数的代码,为这个对象添加属性,方法等;

4.返回新对象。

因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对方返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象,p1。

而原型方法上的this就好理解多了,根据上边对函数中this的定义,p1.getName()中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。

好啦,我所知道的,关于this的一切,已经总结完了,希望大家在阅读之后,能够真正学到东西,然后给我点个赞^_^。如果你发现有什么错误,请在评论中指出,我会尽快修改。先谢过了。

陳述
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
超越瀏覽器:現實世界中的JavaScript超越瀏覽器:現實世界中的JavaScriptApr 12, 2025 am 12:06 AM

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

使用Next.js(後端集成)構建多租戶SaaS應用程序使用Next.js(後端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:23 AM

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

如何使用Next.js(前端集成)構建多租戶SaaS應用程序如何使用Next.js(前端集成)構建多租戶SaaS應用程序Apr 11, 2025 am 08:22 AM

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript:探索網絡語言的多功能性JavaScript:探索網絡語言的多功能性Apr 11, 2025 am 12:01 AM

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的演變:當前的趨勢和未來前景JavaScript的演變:當前的趨勢和未來前景Apr 10, 2025 am 09:33 AM

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

神秘的JavaScript:它的作用以及為什麼重要神秘的JavaScript:它的作用以及為什麼重要Apr 09, 2025 am 12:07 AM

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。

Python還是JavaScript更好?Python還是JavaScript更好?Apr 06, 2025 am 12:14 AM

Python更适合数据科学和机器学习,JavaScript更适合前端和全栈开发。1.Python以简洁语法和丰富库生态著称,适用于数据分析和Web开发。2.JavaScript是前端开发核心,Node.js支持服务器端编程,适用于全栈开发。

如何安裝JavaScript?如何安裝JavaScript?Apr 05, 2025 am 12:16 AM

JavaScript不需要安裝,因為它已內置於現代瀏覽器中。你只需文本編輯器和瀏覽器即可開始使用。 1)在瀏覽器環境中,通過標籤嵌入HTML文件中運行。 2)在Node.js環境中,下載並安裝Node.js後,通過命令行運行JavaScript文件。

See all articles

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

MinGW - Minimalist GNU for Windows

MinGW - Minimalist GNU for Windows

這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

將Eclipse與SAP NetWeaver應用伺服器整合。

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

Dreamweaver Mac版

Dreamweaver Mac版

視覺化網頁開發工具

SublimeText3 Linux新版

SublimeText3 Linux新版

SublimeText3 Linux最新版