JavaScript中的this指向

1.概念

在JavaScript中定踱,this 是指當(dāng)前函數(shù)中正在執(zhí)行的上下文環(huán)境开泽,因?yàn)檫@門(mén)語(yǔ)言擁有四種不同的函數(shù)調(diào)用類(lèi)型:

  • 函數(shù)調(diào)用 alert('hello world!')
  • 方法調(diào)用 console.log('hello world')
  • 構(gòu)造函數(shù)調(diào)用 new RegExp('\\d')
  • 間接調(diào)用 alert.call(undefined, 'hello world')

在以上每一項(xiàng)的調(diào)用上,他們都擁有獨(dú)立的上下文環(huán)境,就會(huì)造成 this 所指意義有所差別。此外,嚴(yán)格模式也會(huì)對(duì)執(zhí)行環(huán)境造成影響院崇。

理解 this 關(guān)鍵字的關(guān)鍵在于理解各種不用的函數(shù)調(diào)用以及他是如何給影響上下文環(huán)境的。
這篇文章旨在解釋不用環(huán)境下的函數(shù)調(diào)用會(huì)怎么影響 this 以及判斷上下文環(huán)境時(shí)會(huì)產(chǎn)生的一些常見(jiàn)陷阱袍祖。

在開(kāi)始描述之前底瓣,先熟悉一下一些術(shù)語(yǔ):

  • 調(diào)用 是執(zhí)行當(dāng)前函數(shù)主體的代碼,即調(diào)用一個(gè)函數(shù)蕉陋。例:parseInt 函數(shù)的調(diào)用為 parseInt(15)
  • 上下文環(huán)境 是方法調(diào)用中 this 所代表的值
  • 作用域 是一系列方法內(nèi)可調(diào)用的變量捐凭,對(duì)象,方法組成的集合

2.函數(shù)調(diào)用

函數(shù)調(diào)用 代表了該函數(shù)接收以成對(duì)的引號(hào)包含凳鬓,用逗號(hào)分隔的不同參數(shù)組成的表達(dá)式茁肠。舉例: parseInt('18') 。這個(gè)表達(dá)式不能是屬性訪(fǎng)問(wèn)如 myObject.myFunction 這個(gè)會(huì)造成方法調(diào)用缩举。[1, 5].join(',') 同樣是方法調(diào)用而不是一個(gè)函數(shù)調(diào)用垦梆。

函數(shù)調(diào)用的一個(gè)簡(jiǎn)單例子:

function hello(name) {
    return 'Hello' + name + '!';
}
// 函數(shù)調(diào)用
var message = hello('World!');
console.log(message); // => 'Hello World!'

hello('World') 是一個(gè)函數(shù)調(diào)用:hello 表達(dá)式代表了一個(gè)函數(shù)對(duì)象,接受了用成對(duì)引號(hào)包含的 World 參數(shù)仅孩。

高級(jí)一點(diǎn)的例子托猩,立即執(zhí)行函數(shù) IIFE(immediately-invoked function expression):

var message = (function(name) {
    return 'Hello ' + name + '!';
})('World');
console.log(message); // => 'Hello World!'

2.1. 函數(shù)調(diào)用中的 this

this is the global object in a function invocation

全局對(duì)象取決于當(dāng)前執(zhí)行環(huán)境,在瀏覽器中辽慕,全局對(duì)象即 window京腥。

在函數(shù)調(diào)用中,上下文執(zhí)行環(huán)境是全局對(duì)象溅蛉,可以在以下函數(shù)中驗(yàn)證上下文:

function sum(a, b) {
    console.log(this === window); // => true
    this.myNumber = 20; // 在全局對(duì)象中添加 'myNumber' 屬性
    return a + b;
}
// sum() 為函數(shù)調(diào)用
// this 在 sum() 中是全局對(duì)象(window)
sum(15, 16); // => 31
window.myNumber; // => 20

當(dāng) sum(15, 16) 被調(diào)用時(shí)绞旅,JavaScript 自動(dòng)將 this 設(shè)置為全局對(duì)象,即 window温艇。

當(dāng) this 在任何函數(shù)作用域以外調(diào)用時(shí)(最外層作用域:全局執(zhí)行上下文環(huán)境)因悲,也會(huì)涉及到全局對(duì)象。

console.log(this === window); // => true
this.myString = 'Hello World!';
console.log(window.myString); // => 'Hello World!'

2.2. 嚴(yán)格模式下勺爱,函數(shù)調(diào)用中的 this

this is undefined in a function invocation in strict mode

嚴(yán)格模式由 ECMAScript 5.1 引進(jìn)晃琳,用來(lái)限制 JavaScript 的一些異常處理,提供更好的安全性和更強(qiáng)壯的錯(cuò)誤檢查機(jī)制琐鲁。使用嚴(yán)格模式卫旱,只需要將 'use strict' 置于函數(shù)體的頂部。這樣就可以將上下文環(huán)境中的 this 轉(zhuǎn)為 undefined围段。這樣執(zhí)行上下文環(huán)境不再是全局對(duì)象顾翼,與非嚴(yán)格模式剛好相反。

在嚴(yán)格模式下執(zhí)行函數(shù)的一個(gè)例子:

function multiply(a, b) {
'use strict'; // 開(kāi)啟嚴(yán)格模式
console.log(this === undefined); // => true
return a * b;
}
// 嚴(yán)格模式下的函數(shù)調(diào)用 multiply()
// this 在 multiply() 中為 undefined
multiply(2, 5); // => 10

當(dāng) multiply(2, 5) 執(zhí)行時(shí)奈泪,這個(gè)函數(shù)中的 thisundefined适贸。

嚴(yán)格模式不僅在當(dāng)前作用域起到作用灸芳,它還會(huì)影響內(nèi)部作用域,即內(nèi)部聲明的一切內(nèi)部函數(shù)的作用域拜姿。

function execute() {
    'use strict'; // 開(kāi)啟嚴(yán)格模式
    function concat(str1, str2) {
        // 內(nèi)部函數(shù)也是嚴(yán)格模式
        console.log(this === undefined); // => true
        return str1 + str2;
    }
    // 在嚴(yán)格模式下調(diào)用concat()
    // this 在 concat() 下是 undefined
    concat('hello', 'world'); // => 'hello world'
}
execute();

一個(gè)簡(jiǎn)單的 JavaScript 文件可能同時(shí)包含嚴(yán)格模式和非嚴(yán)格模式烙样,所以在用一種類(lèi)型調(diào)用中,可能也會(huì)有不同的上下文差異蕊肥。

function nonStrictSum(a, b) {
    // 非嚴(yán)格模式
    console.log(this === window); // => true
    return a + b;
}
function strictSum(a, b) {
    'use strict';
    // 嚴(yán)格模式
    console.log(this === undefined); // => true
    return a + b;
}
// noStrictSum() 在非嚴(yán)格模式下調(diào)用
// this 在 nonStrictSum() 中是 window 對(duì)象
nonStrictSum(5, 6) // => 11
// strictSum() 在嚴(yán)格模式下被調(diào)用
// this 在strictSum() 中是 undefined
strictSum(8, 12); // => 20

2.3 陷阱: this 在內(nèi)部函數(shù)中

一個(gè)常見(jiàn)的陷阱是理所應(yīng)當(dāng)?shù)恼J(rèn)為函數(shù)調(diào)用的谒获,內(nèi)部函數(shù)中 this 等同于它的外部函數(shù)中的 this

正確的理解是內(nèi)部函數(shù)的上下文環(huán)境取決于調(diào)用環(huán)境壁却,而不是外部函數(shù)的上下文環(huán)境批狱。

為了獲取到所期望的 this,應(yīng)該利用間接調(diào)用修改內(nèi)部函數(shù)的上下文環(huán)境展东,如使用 .call() 或者 apply() 或者創(chuàng)建一個(gè)綁定函數(shù) .bind()精耐。

下面的例子表示計(jì)算兩個(gè)數(shù)之和:

var numbers = {
    numberA: 5,
    numberB: 10,
    sum: function () {
        console.log(this === numbers); // => true
        function calculate() {
            // 嚴(yán)格模式下,this 是 window or undefined
            console.log(this === numbers); // => false
            return this.numberA + this.numberB;
        }
        return calculate();
    }
};
numbers.sum(); // => 嚴(yán)格模式下琅锻,結(jié)果為 NaN 或者 throws TypeError

numbers.sum() 是對(duì)象內(nèi)的一個(gè)方法調(diào)用卦停,因此 sum 的上下文是 numbers 對(duì)象,而 calculate 函數(shù)定義在 sum 函數(shù)內(nèi)恼蓬,所以會(huì)誤以為在 calculate 內(nèi) this 也指向的是 numbers惊完。

然后 calculate() 在函數(shù)調(diào)用(而不是作為方法調(diào)用)時(shí),此時(shí)的 this 指向的是全局對(duì)象 window 或者在嚴(yán)格模式下指向的是 undefined处硬,即使外部函數(shù) sum 擁有 numbers對(duì)象作為上下文環(huán)境小槐,它也沒(méi)有辦法影響到內(nèi)部的 this

numbers.sum() 調(diào)用的結(jié)果是 NaN 或者在嚴(yán)格模式下直接拋出錯(cuò)誤 TypeError:Cannot read property 'numberA' of undefined荷辕,而絕非期待的結(jié)果 5 + 10 = 15凿跳,造成這樣的原因是 calculate 并沒(méi)有正確的被調(diào)用。

為了解決這個(gè)問(wèn)題疮方,正確的做法是使 calculate 函數(shù)被調(diào)用時(shí)的上下文同 sum 調(diào)用時(shí)一樣控嗜,為了得到屬性 numberAnumberB,其中一種方法是使用 .call() 方法骡显。

var numbers = {
    numberA: 5,
    numberB: 10,
    sum: function () {
        console.log(this === numbers); // => true
        function calculate() {
            console.log(this === numbers); // =>true
            return this.numberA + this.numberB;
        }
        // 使用 .call() 方法修改上下文環(huán)境
        return calculate.call(this);
    }
};
numbers.sum(); // => 15

calculate.call(this) 同樣執(zhí)行了 calculate 函數(shù)疆栏,但是格外的添加了 this 作為第一個(gè)參數(shù),修改上下文執(zhí)行環(huán)境惫谤。此時(shí)的 this.numberA + this.numberB 等同于 numbers.numberA + numbers.numberB壁顶,其最終的結(jié)果就會(huì)如期盼的一樣為 5 + 10 = 15

3.方法調(diào)用

方法是作為一個(gè)對(duì)象屬性存儲(chǔ)的函數(shù)溜歪,舉個(gè)例子:

var myObject = {
    // helloFunction 是對(duì)象中的方法
    helloFunction: function() {
        return 'hello world';
    }
};
var message = myObject.helloFunction();

helloFunction 是屬于 myObject 的一個(gè)方法若专,調(diào)用這個(gè)方法可以使用屬性訪(fǎng)問(wèn)的方式 myObject.helloFunction

方法調(diào)用表現(xiàn)為對(duì)象屬性訪(fǎng)問(wèn)的形式蝴猪,支持傳入用成對(duì)引號(hào)包裹起來(lái)的一系列參數(shù)。上個(gè)例子中,myObject.helloFunction() 其實(shí)就是對(duì)象 myObject 上對(duì)屬性 helloFunction 的方法調(diào)用启搂。同樣 [1, 2].join(',')/\s/.test('beautiful world') 都是方法調(diào)用。

區(qū)分函數(shù)調(diào)用和方法調(diào)用是非常重要的,它們是不同類(lèi)型的調(diào)用方式动壤。主要的差別在于方法調(diào)用為訪(fǎng)問(wèn)屬性的形式萝喘,如:.functionProperty() 或者 'functionProperty',而函數(shù)調(diào)用為()```琼懊。

['hello', 'world'].join(','); // 方法調(diào)用
({ten: function() {return 10;} }).ten(); // 方法調(diào)用
var obj = {};
obj.myFunction = function() {
    return new Date().toString();
};
obj.myFunction(); // 方法調(diào)用

var otherFunction = obj.myFunction;
otherFunction(); // 函數(shù)調(diào)用
parseFloat('16.60'); // 函數(shù)調(diào)用
isNaN(0); // 函數(shù)調(diào)用

3.1. 方法調(diào)用中的 this

this is the object that owns the method in a method invocation

當(dāng)在一個(gè)對(duì)象里調(diào)用方法時(shí)阁簸,this 代表的是對(duì)象它自身。讓我們創(chuàng)建一個(gè)對(duì)象哼丈,其包含一個(gè)可以遞增屬性的方法启妹。

var calc = {
    num: 0,
    increment: function() {
        console.log(this === calc); // => true
        this.num += 1;
        return this.num;
    }    
};
// 方法調(diào)用,this 指向 calc
calc.increment(); // => 1
calc.increment(); // => 2

calc.increment() 調(diào)用意味著上下文執(zhí)行環(huán)境在 calc 對(duì)象里醉旦,因此使用 this.num 遞增 num 這個(gè)屬性的可行的饶米。

一個(gè) JavaScript 對(duì)象繼承方法來(lái)自于它自身的屬性。當(dāng)一個(gè)被繼承方法在對(duì)象中調(diào)用時(shí)车胡,上下文執(zhí)行環(huán)境同樣是對(duì)象本身檬输。

var myDog = Object.create({
    sayName: function() {
        console.log(this === myDog); // => true
        return this.name;
    }
});
myDog.name = 'Milo';
// 方法調(diào)用,this 指向 myDog
myDog.sayname(); // => 'Milo';

Object.create() 創(chuàng)建了一個(gè)新的對(duì)象 myDog 并且設(shè)置了屬性匈棘,myDog 對(duì)象繼承了 myName方法丧慈。當(dāng) myDog.sayName() 被執(zhí)行時(shí),上下文執(zhí)行環(huán)境指向 myDog主卫。

在ECMAScript 5 的 class 語(yǔ)法中逃默,方法調(diào)用指的是實(shí)力本身。

class Planet {
    constructor(name) {
        this.name = name;
    }
    getName() {
        console.log(this === earth); // => true
        return this.name;
    }
}
var earth = new Planet('Earth');
// 方法調(diào)用簇搅,上下文為 earth
earth.getName(); // => 'Earth' 

3.2. 陷阱:方法會(huì)分離它自身的對(duì)象

一個(gè)對(duì)象中的方法可能會(huì)被提起抽離成一個(gè)變量完域。當(dāng)使用這個(gè)變量調(diào)用方法時(shí),開(kāi)發(fā)者可能會(huì)誤認(rèn)為 this 指向的還是定義該方法時(shí)的對(duì)象瘩将。

如果方法調(diào)用不依靠對(duì)象筒主,那么就是一個(gè)函數(shù)調(diào)用,即 this 指向全局對(duì)象 object 或者在嚴(yán)格模式下為 undefined鸟蟹。創(chuàng)建函數(shù)綁定可以修復(fù)上下文乌妙,使該方法被正確對(duì)象調(diào)用。

下面的例子創(chuàng)建了構(gòu)造器函數(shù) Animal 并且創(chuàng)建了一個(gè)實(shí)例 myCat建钥,在 setTimeout() 定時(shí)器 1s 后打印 myCat 對(duì)象信息

function Animal(type, legs) {
    this.type = type;
    this.legs = legs;
    this.logInfo = function() {
        console.log(this === myCat); // => false
        console.log('The ' + this.type + ' has ' + this.legs + ' legs');
    }
}
var myCat = new Animal('Cat', 4);
// 打印出 "The undefined has undefined legs"
// 或者在嚴(yán)格模式下拋出錯(cuò)誤 TypeError
setTimeout(myCat.logInfo, 1000);

開(kāi)發(fā)者可能認(rèn)為在 setTimeout 下調(diào)用 myCat.logInfo() 會(huì)打印出 myCat 對(duì)象的信息藤韵。但實(shí)際上這個(gè)方法被分離了出來(lái)作為了參數(shù)傳入函數(shù)內(nèi) setTimeout(myCat.logInfo),然后 1s 后會(huì)發(fā)生函數(shù)調(diào)用熊经。當(dāng) logInfo 被作為函數(shù)調(diào)用時(shí)泽艘,this 指向全局對(duì)象 window 或者在嚴(yán)格模式下為 undefined欲险,因?yàn)閷?duì)象信息沒(méi)有正確地被打印。

方法綁定可以使用 .bind() 方法匹涮。如果被分離的方法綁定了 myCat 對(duì)象天试,那么上下文問(wèn)題就可以被解決了:

function Animal(type, legs) {
    this.type = type;
    this.legs = legs;
    this.logInfo = function() {
        console.log(this === myCat); // => true
        console.log('The ' + this.type + ' has ' + this.legs + ' legs');
    }
}
var myCat = new Animal('Cat', 4);
// 打印出 "The Car has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);

此時(shí), myCat.logInfo.bind(myCat) 返回的新函數(shù)調(diào)用里的 this 指向了 myCat然低。

4. 構(gòu)造函數(shù)調(diào)用

構(gòu)造函數(shù)調(diào)用使用 new 關(guān)鍵詞喜每,后面跟隨可帶參數(shù)的對(duì)象表達(dá)式,例: new RegExp('\\d')雳攘。

以下的例子聲明了一個(gè)構(gòu)造函數(shù) Country带兜,并調(diào)用。

function Country(name, traveled) {
    this.name = name ? this.name : 'United Kingdom';
    this.traveled = Boolean(traveled); // 轉(zhuǎn)換為 boolean 值
}
Country.prototype.travel = function() {
    this.traveled = true;
};
// 構(gòu)造函數(shù)調(diào)用
var france = new Country('France', false);
// 構(gòu)造函數(shù)調(diào)用
var unitedKingdom = new Country;

france.travel(); // Travel to France

new City('Paris') 是一個(gè)構(gòu)造器調(diào)用吨灭,這個(gè)對(duì)象初始化使用了類(lèi)中特殊的方法 constructor 刚照,其中的 this 指向的是新創(chuàng)建的對(duì)象。

構(gòu)造器調(diào)用創(chuàng)建了一個(gè)空的新對(duì)象喧兄,從構(gòu)造器的原型中繼承屬性无畔。這個(gè)構(gòu)造器函數(shù)的意義在于初始化對(duì)象,因此這個(gè)類(lèi)型的函數(shù)調(diào)用創(chuàng)建實(shí)例吠冤。

當(dāng)一個(gè)屬性訪(fǎng)問(wèn) myObject.myFunction 前擁有 new 關(guān)鍵詞檩互,那么 JavaScript 會(huì)執(zhí)行構(gòu)造器調(diào)用而不是方法調(diào)用。舉個(gè)例子: new myObject.myFunction() 意味著首先這個(gè)函數(shù)會(huì)解析為一個(gè)屬性訪(fǎng)問(wèn)函數(shù) extractedFunction = myObject.myFunction 咨演,然后用構(gòu)造器創(chuàng)建一個(gè)新對(duì)象 new extractedFunction闸昨。

4.1. 在構(gòu)造函數(shù)調(diào)用中的 this

this is the newly created object in a constructor invocation

構(gòu)造器調(diào)用的環(huán)境是新創(chuàng)建的對(duì)象。通過(guò)傳遞構(gòu)造函數(shù)參數(shù)來(lái)初始化新建的對(duì)象薄风,添加屬性初始化值以及事件處理器饵较。

讓我們來(lái)驗(yàn)證以下這個(gè)例子的上下文環(huán)境:

function Foo() {
    console.log(this instancof Foo); // => true
    this.property = 'Default Value';
}
// 構(gòu)造函數(shù)調(diào)用
var fooInstance = new Foo();
fooInstance.property; // => 'Default Value'

new Foo() 建立構(gòu)造器調(diào)用,它的上下文環(huán)境為 fooInstance 遭赂,在 Foo 對(duì)象中初始化了 this.property 這個(gè)屬性并賦予初始值循诉。

在使用 class 語(yǔ)法時(shí)也是同樣的情況(在 ES6 中),初始化只發(fā)生在它的 constructor 方法中撇他。

class Bar {
    constructor() {
        console.log(this instanceof Bar); // => true
        this.property = 'Default Value';
    }
}
// 構(gòu)造函數(shù)調(diào)用
var barInstance = new Bar();
barInstance.property; // => 'Default Value'

當(dāng)執(zhí)行 new Bar() 時(shí)茄猫,JavaScript 創(chuàng)建了一個(gè)空對(duì)象并且它的上下文環(huán)境為 constructor 方法,因此添加屬性方法是使用 this 關(guān)鍵詞:this.property = 'Default Value'困肩。

4.2. 陷阱:忘記添加 new 關(guān)鍵詞

一些 JavaScript 函數(shù)創(chuàng)建實(shí)例划纽,不僅僅可以使用構(gòu)造器的形式調(diào)用也可以利用函數(shù)調(diào)用,下面是一個(gè) RegExp 的例子:

var reg1 = new RegExp('\\w+');
var reg2 = RegExp('\\w+');

reg1 instanceof RegExp; // => true
reg2 instanceof RegExp; // => true
reg1.source === reg2.source; // => true

當(dāng)執(zhí)行 new RegExp('\\w+')RegExp('\\w+') 時(shí)锌畸,JavaScript 創(chuàng)建了兩個(gè)相等的普通表達(dá)式對(duì)象勇劣。

但是使用函數(shù)調(diào)用創(chuàng)建對(duì)象會(huì)產(chǎn)生潛在的問(wèn)題(包括工廠(chǎng)模式),當(dāng)失去了 new 關(guān)鍵詞,一些構(gòu)造器會(huì)取消初始化對(duì)象比默。

以下例子描述了這個(gè)問(wèn)題:

function Vehicle(type, wheelsCount) {
    this.type = type;
    this.wheelsCount = wheelsCount;
    return this;
}
// 函數(shù)調(diào)用
var car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount; // => 4
car === window // =>true

Vehicle 是一個(gè)在對(duì)象上設(shè)置了 typewheelsCount 屬性的函數(shù)幻捏。

當(dāng)執(zhí)行了 Vehicle('Car', 4) 時(shí),會(huì)返回對(duì)象 car命咐,它擁有正確的屬性值:
car.type 指向 Car篡九,car.wheelsCount 指向 4,開(kāi)發(fā)者誤以為這樣創(chuàng)建初始化對(duì)象沒(méi)有什么問(wèn)題醋奠。
然而榛臼,當(dāng)前執(zhí)行的是函數(shù)調(diào)用,因此 this 指向的是 window 對(duì)象钝域,所以它設(shè)置的屬性其實(shí)是掛在 window 對(duì)象上的讽坏,這樣是完全錯(cuò)誤的锭魔,它并沒(méi)有創(chuàng)建一個(gè)新對(duì)象例证。

應(yīng)該正確的執(zhí)行方式是使用 new 關(guān)鍵詞來(lái)保證構(gòu)造器被正確調(diào)用:

function Vehicle(type, wheelsCount) {
    if (!(this instance of Vehicle)) {
        throw Error('Error: Incorrect invocation');
    }
    this.type = type;
    this.wheelsCount = wheelsCount;
    return this;
}

// 構(gòu)造函數(shù)調(diào)用
var car = new Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount; // => 4
car instanceof Vehicle // => true

// 函數(shù)調(diào)用,會(huì)報(bào)錯(cuò)
var brokenCat = Vehicle('Broken Car', 3);

new Vehicle('Car', 4) 可以正確運(yùn)行:一個(gè)新的對(duì)象被創(chuàng)建和初始化迷捧,因?yàn)?new 關(guān)系詞代表了當(dāng)前為構(gòu)造器調(diào)用织咧。
在構(gòu)造器函數(shù)中添加驗(yàn)證:this instanceof Vehicle ,可以保證當(dāng)前的執(zhí)行上下文是正確的對(duì)象類(lèi)型漠秋。如果this 不是指向 Vehicle笙蒙,那么就存在錯(cuò)誤。如果 Vehicle('Cat', 4) 表達(dá)式?jīng)]有 new 關(guān)鍵詞而被執(zhí)行庆锦,就會(huì)拋出錯(cuò)誤:Error:Incorrect invocation捅位。

5. 間接調(diào)用

間接調(diào)用表現(xiàn)為當(dāng)一個(gè)函數(shù)使用了 .call() 或者 .apply() 方法。

在 JavaScript 中搂抒,函數(shù)為一等對(duì)象艇搀,這意味著函數(shù)是一個(gè)對(duì)象,對(duì)象類(lèi)型即為 Function求晶。
函數(shù)的一系列方法中焰雕, .call().apply() 被用來(lái)配置當(dāng)前調(diào)用的上下文環(huán)境。

方法 .call(thisArg[, arg1[, arg2, [, ...]]]) 接受第一個(gè)參數(shù) thisArg 作為執(zhí)行的上下文環(huán)境芳杏,以及一系列參數(shù) arg1, arg2, ... 作為函數(shù)的傳參被調(diào)用矩屁。
并且,方法 .apply(thisArg, [args]) 接收 thisArg 作為上下文環(huán)境爵赵,剩下的參數(shù)可以用類(lèi)數(shù)組對(duì)象 [args] 傳遞吝秕。

間接調(diào)用的例子:

function increment(number) {
    return ++ number;
}
increment.call(undefined, 10); // => 11
increment.apply(undefined, [10]); // => 11

increment.call()increment.apply() 同時(shí)傳遞了參數(shù) 10 調(diào)用 increment 函數(shù)。

兩個(gè)方法最主要的區(qū)別為 .call() 接收一組參數(shù)空幻,如 myFunction.call(thisValue, 'value1', 'value2') 郭膛,而 .apply() 接收一串參數(shù)作為類(lèi)數(shù)組對(duì)象傳遞,如 myFunction.apply(thisValue, ['value1', 'value2']);氛悬。

5.1.間接調(diào)用中的 this

this is the first argument of .call() or .apply() in an indirect invocation

很明顯则剃,在間接調(diào)用中耘柱,this 指向的是 .call().apply() 傳遞的第一個(gè)參數(shù)。

var rabbit = { name: 'white Rabbit' };
function concatName(string) {
    console.log(this === rabbit);
    return string + this.name;
}
// 間接調(diào)用
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye white Rabbit'

當(dāng)函數(shù)執(zhí)行需要特別指定上下文時(shí)棍现,間接調(diào)用非常有用调煎,它可以解決函數(shù)調(diào)用中的上下文問(wèn)題(this 指向 window 或者嚴(yán)格模式下指向 undefined),同時(shí)也可以用來(lái)模擬方法調(diào)用對(duì)象己肮。

另一個(gè)實(shí)踐例子為士袄,在 ES5 中的類(lèi)繼承中,調(diào)用父級(jí)構(gòu)造器谎僻。

function Runner(name) {
    console.log(this instanceof Rabbit); // => true
    this.name = name;
}
function Rabbit(name, countLegs) {
    console.log(this instanceof Rabbit); // => true
    // 間接調(diào)用娄柳,調(diào)用了父級(jí)構(gòu)造器
    Runner.call(this, name);
    this.countLegs = countLegs;
}
var myRabbit = new Rabbit('white Rabbit', 4);
myRabbit; // { name: 'white Rabbit', countLegs: 4 }

Runner.call(this, name)Rabbit 里間接調(diào)用了父級(jí)方法初始化對(duì)象。

6. 綁定函數(shù)調(diào)用

綁定函數(shù)調(diào)用是將函數(shù)綁定一個(gè)對(duì)象艘绍,它是一個(gè)原始函數(shù)使用了 .bind() 方法赤拒。原始綁定函數(shù)共享相同的代碼和作用域,但是在執(zhí)行時(shí)擁有不同的上下文環(huán)境诱鞠。

方法 .bind(thisArg[, arg1[,arg2[, ...]]]) 接收第一個(gè)參數(shù) thisArg 作為綁定函數(shù)在執(zhí)行時(shí)的上下文環(huán)境挎挖,以及一組參數(shù) arg1, arg2, ... 作為傳參傳入函數(shù)中。它返回一個(gè)新的函數(shù)航夺,綁定了 thisArg蕉朵。

下列代碼創(chuàng)建了一個(gè)綁定函數(shù)并在之后被調(diào)用:

function multiply(number) {
    'use strict'
    return this * number;
}
// 創(chuàng)建綁定函數(shù),綁定上下文2
var double = multiply.bind(2);
// 調(diào)用間接調(diào)用
double(3); // => 6
double(10); // => 20

multiply.bind(2) 返回一個(gè)新的函數(shù)對(duì)象 double阳掐,它綁定了數(shù)字 2始衅。multiplydouble 函數(shù)擁有相同代碼和作用域。

對(duì)比方法 .apply().call() 缭保,它倆都立即執(zhí)行了函數(shù)汛闸,而 .bind() 函數(shù)返回了一個(gè)新方法,綁定了預(yù)先指定好的 this涮俄,并可以延后調(diào)用蛉拙。

6.1. 綁定函數(shù)中的 this

this is the first argument of .bind() when invoking a bound function

.bind() 方法的作用是創(chuàng)建一個(gè)新的函數(shù),執(zhí)行時(shí)的上下文環(huán)境為 .bind() 傳遞的第一個(gè)參數(shù)彻亲,它允許創(chuàng)建預(yù)先設(shè)置好 this 函數(shù)孕锄。

讓我們來(lái)看看在綁定函數(shù)中如何設(shè)置 this

var numbers = {
    array: [3, 5, 10],
    getNumbers: function () {
        return this.array;
    }
};
// 創(chuàng)建一個(gè)綁定函數(shù)
var boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// 從對(duì)象中抽取方法
var simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined 或者嚴(yán)格模式下拋出錯(cuò)誤

numbers.countNumbers.bind(numbers) 返回了綁定 numbers 對(duì)象的函數(shù) boundGetNumbers,它在調(diào)用時(shí)的 this 指向的是 numbers 并且返回正確的數(shù)組對(duì)象苞尝。

.bind() 創(chuàng)建了一個(gè)永恒的上下文鏈并不可修改畸肆。一個(gè)綁定函數(shù)即使使用了 .call() 或者 .apply() 傳入其他不同的上下文環(huán)境,也不會(huì)更改它之前連接的上下文環(huán)境宙址,重新綁定也不會(huì)起任何作用轴脐。

只有在構(gòu)造器調(diào)用時(shí),綁定函數(shù)可以改變上下文,然而并不是特別推薦的做法大咱。

下面這個(gè)例子聲明了一個(gè)綁定函數(shù)恬涧,然后試圖更改其預(yù)定上下文的情況:

function getThis() {
    'use strict`;
    return this;
}
var one = getThis.bind(1);
// 綁定函數(shù)調(diào)用
one(); // => 1
// 使用 .apply() 和 .call() 綁定函數(shù)
one.call(2); // => 1
one.apply(2); // => 1
// 重新綁定
one.bind(2); // => 1
// 利用構(gòu)造器方式調(diào)用綁定函數(shù)
new one(); // => object

只有 new one() 時(shí)可以改變綁定函數(shù)的上下文環(huán)境,其他類(lèi)型的調(diào)用結(jié)果是 this 永遠(yuǎn)指向 1碴巾。

7. 箭頭函數(shù)

箭頭函數(shù)的設(shè)計(jì)意圖是以精簡(jiǎn)的方式創(chuàng)建函數(shù)溯捆,并綁定定義時(shí)的上下文環(huán)境。

var hello = (name) => {
    return 'Hello ' + name;
};
hello('World'); // => 'Hello World'
// 保留偶數(shù)
[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]

箭頭函數(shù)使用了輕便的語(yǔ)法厦瓢,去除了關(guān)鍵詞 function 的書(shū)寫(xiě)提揍,甚至當(dāng)函數(shù)只有一個(gè)句子時(shí),可以省去 return 不寫(xiě)煮仇。

箭頭函數(shù)是匿名的劳跃,意味著函數(shù)的屬性 name 是一個(gè)空字符串 '',它沒(méi)有一個(gè)詞匯式的函數(shù)名浙垫,意味著不利于使用遞歸或者解除事件處理刨仑。

同時(shí)它不同于普通函數(shù),它不提供 arguments 對(duì)象绞呈,在 ES6 中可以用另外的參數(shù)代替:

var sumArguments = (...args) => {
    console.log(typeof arguments); // => 'undefined'
    return args.reduce((result, item) => result + item);
};
sumArguments.name; // => ''
sumArguments(5, 5, 6); // => 16

7.1. 箭頭函數(shù)中的 this

this is the enclosing context where the arrow function is defined

箭頭函數(shù)并不創(chuàng)建它自身執(zhí)行的上下文贸人,使得 this 取決于它在定義時(shí)的外部函數(shù)间景。

下面的例子表示了上下文的透明屬性:

class Point {
    constructor(x, y) {
        this.x = x;
        this.y;
    }
    log() {
        console.log(this === myPoint); // => true
        setTimeout( () => {
            console.log(this === myPoint); // => true
            console.log(this.x + ':' + this.y); // => '95:165'
        }, 1000);
    }
}

setTimeout 調(diào)用了箭頭函數(shù)佃声,它的上下文和 log() 方法一樣都是 myPont 對(duì)象√纫可以看出來(lái)圾亏,箭頭函數(shù)“繼承”了它在定義時(shí)的函數(shù)上下文。

如果嘗試在上述例子中使用正常函數(shù)封拧,那么它會(huì)創(chuàng)建自身的作用域(window 或者嚴(yán)格模式下 undefined)志鹃。因此,要使同樣的代碼可以正確運(yùn)行就必須人工綁定上下文泽西,即 setTimeout(function() {...}.bind(this))曹铃。使用箭頭函數(shù)就可以省略這么詳細(xì)的函數(shù)綁定,用更加干凈簡(jiǎn)短的代碼綁定函數(shù)捧杉。

如果箭頭函數(shù)在最外層作用域定義陕见,那么上下文環(huán)境將永遠(yuǎn)是全局對(duì)象,一般來(lái)說(shuō)在瀏覽器中即為 window味抖。

var getContext = () => {
    console.log(this === window); // => true
    return this;
};
console.log(getContext() === window); // => true

箭頭函數(shù)一次綁定上下文后便不可更改评甜,即使使用了上下文更改的方法:

var numbers = [1, 2];
(function() {
    var get = () => {
        console.log(this === numbers); // => true
        return this;
    }
    console.log(this === numbers); // => true
    get(); // => [1, 2]
    // 箭頭函數(shù)使用 .apply() 和 .call()
    get.call(0); // => [1 ,2]
    get.apply(0); // => [1, 2]
    // Bind
    get.bind([0])(); // => [1, 2]
}).call(numbers);

函數(shù)表達(dá)式可以間接調(diào)用 .call(numbers)this 指向 numbers,然而 get 箭頭函數(shù)的 this 也是指向 numbers 的仔涩,因?yàn)樗壎硕x時(shí)的外部函數(shù)忍坷。

無(wú)論怎么調(diào)用 get 函數(shù),它的初始化上下文始終是 numbers,間接地調(diào)用其他上下文(使用 .call() 或者 .apply())佩研,或者重新綁定上下文(使用 .bind())都沒(méi)有任何作用柑肴。

箭頭函數(shù)不可以用作構(gòu)造器,如果使用 new get() 作構(gòu)造器調(diào)用旬薯,JavaScript 會(huì)拋出錯(cuò)誤:TyperError: get is not a constructor嘉抒。

7.2. 陷阱:使用箭頭函數(shù)定義方法

開(kāi)發(fā)者可能會(huì)想使用箭頭函數(shù)在對(duì)象中聲明方法,箭頭函數(shù)的聲明((param) => {...})要比函數(shù)表達(dá)式的聲明(function(param) {...})簡(jiǎn)短的多袍暴。

下面的例子在類(lèi) Period 中 使用箭頭函數(shù)定義了方法 format()

function Period (hours, minutes) {
    this.hour = hours;
    this.minutes = minutes;
}
Period.prototype.format = () => {
    console.log(this === window); // => true
    return this.hours + ' hours and ' + this.minutes + ' minutes';
};
var walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'

當(dāng) format 是一個(gè)箭頭函數(shù)些侍,且被定義在全局環(huán)境下,它的 this 指向的是 window 對(duì)象政模。

即使 format 執(zhí)行的時(shí)候掛載在對(duì)象上 walkPeriod.format()岗宣,window 對(duì)象依舊存在在調(diào)用的上下文環(huán)境下。這是因?yàn)榧^函數(shù)擁有靜態(tài)的上下文環(huán)境淋样,不會(huì)因?yàn)椴煌恼{(diào)用而改變耗式。

this 指向的是 window,因此 this.hourthis.minutes 都是 undefined趁猴。方法返回的結(jié)果為:'undefined hours and undefined minutes'刊咳。

正確的函數(shù)表達(dá)式可以解決這個(gè)問(wèn)題,因?yàn)槠胀ê瘮?shù)可以改變調(diào)用時(shí)的上下文環(huán)境:

function Period(hours, minutes) {
    this.hour = hours;
    this.minutes = minutes;
}
Period.prototype.format = function() {
    console.log(this === walkPeriod); // => true
    return this.hours + ' hours and ' + this.minutes + ' minutes';
};
var walkPeriod = new Period(2. 30);
walkPeriod.format(); // => '2 hours and 30 minutes'

walkPeriod.format() 是一個(gè)在對(duì)象中的方法調(diào)用儡司,它的上下文環(huán)境為 walkPeriod娱挨,this.hours 指向 2this.minutes 指向的是 30捕犬,因此可以返回正確的結(jié)果:'2 hours and 30 minutes'跷坝。

8. 結(jié)尾

因?yàn)楹瘮?shù)調(diào)用會(huì)極大地影響到 this,所以從現(xiàn)在開(kāi)始不要直接問(wèn)自己:

this 是從哪里來(lái)的碉碉?

而是要開(kāi)始思考:

當(dāng)前函數(shù)是怎么被調(diào)用的柴钻?

遇到箭頭函數(shù)時(shí),考慮:

當(dāng)箭頭函數(shù)被定義時(shí)垢粮,this 是指向什么贴届?

以上思路可以幫助開(kāi)發(fā)者減少判斷 this 帶來(lái)的煩惱。

9. 總結(jié)

  1. 在 JavaScript 中蜡吧,this 是指當(dāng)前函數(shù)中正在執(zhí)行的上下文環(huán)境
  2. 四種函數(shù)調(diào)用類(lèi)型:函數(shù)調(diào)用毫蚓、方法調(diào)用、構(gòu)造函數(shù)調(diào)用斩跌、間接調(diào)用
  3. 在函數(shù)調(diào)用中绍些,上下文執(zhí)行環(huán)境為全局對(duì)象。在瀏覽器中耀鸦,全局對(duì)象即為 window柬批。但是在嚴(yán)格模式下上下文環(huán)境中的 this 轉(zhuǎn)為 undefined
  4. 內(nèi)部函數(shù)的上下文環(huán)境取決于調(diào)用環(huán)境啸澡,而不是外部函數(shù)的上下文環(huán)境
  5. 在方法調(diào)用中,當(dāng)在一個(gè)對(duì)象里調(diào)用方法時(shí)氮帐,this 代表的是對(duì)象它本身嗅虏,在 ECMAScript 5 的 class 語(yǔ)法中,方法調(diào)用指的是實(shí)例本身上沐。
  6. 注意:一個(gè)對(duì)象中的方法可能會(huì)被提取抽離成一個(gè)變量(比如上文中 setTimeout(myCat.logInfo), 1000) 的例子皮服。當(dāng)使用這個(gè)變量調(diào)用方法時(shí)會(huì)變成一個(gè)函數(shù)調(diào)用,解決方式是創(chuàng)建函數(shù)綁定参咙。
  7. 構(gòu)造函數(shù)調(diào)用:欣慰構(gòu)造器調(diào)用創(chuàng)建了一個(gè)空的對(duì)象龄广,然后從構(gòu)造器的原型中繼承屬性,所以 this 是指向新創(chuàng)建的對(duì)象蕴侧。
  8. 間接調(diào)用表現(xiàn)為當(dāng)一個(gè)函數(shù)使用了 .call() 或者 .apply() 方法择同。其中的上下文環(huán)境都是 .call().apply() 接收的第一個(gè)參數(shù)。當(dāng)函數(shù)執(zhí)行需要特別指定上下文時(shí)净宵,間接調(diào)用非常有用敲才,它可以解決函數(shù)調(diào)用中的上下文問(wèn)題,同時(shí)也可以用來(lái)模擬方法調(diào)用對(duì)象择葡。
  9. 綁定函數(shù)調(diào)用時(shí)將函數(shù)綁定為一個(gè)對(duì)象紧武,它是一個(gè)原始函數(shù)使用了 .bind() 方法。原始綁定函數(shù)共享相同的代碼和作用域敏储,但是在執(zhí)行時(shí)擁有不同的上下文環(huán)境阻星。其中的上下文環(huán)境是 .bind() 方法接收的第一個(gè)參數(shù)。對(duì)比方法 .apply().call()虹曙,他倆都立即執(zhí)行了函數(shù)迫横,而 .bind() 函數(shù)返回了一個(gè)新方法番舆,綁定了預(yù)先定義好的 this酝碳,并可以延后調(diào)用。
  10. 注意:.bind() 創(chuàng)建了一個(gè)永恒的上下文鏈并不可修改恨狈。只有在構(gòu)造器調(diào)動(dòng)時(shí)疏哗,綁定函數(shù)可以改變上下文,然后這并不是特別推薦的做法禾怠。
  11. 箭頭函數(shù)的設(shè)計(jì)意圖是以精簡(jiǎn)的方式創(chuàng)建函數(shù)返奉,并綁定定義時(shí)的上下文環(huán)境。箭頭函數(shù)并不創(chuàng)建它自身執(zhí)行的上下文吗氏,使得 this 取決于它在定義時(shí)的外部函數(shù)芽偏。
  12. 注意:箭頭函數(shù)擁有靜態(tài)的上下文環(huán)境,不會(huì)因?yàn)椴挥玫恼{(diào)用而改變弦讽。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末污尉,一起剝皮案震驚了整個(gè)濱河市膀哲,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌被碗,老刑警劉巖某宪,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異锐朴,居然都是意外死亡兴喂,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)焚志,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)衣迷,“玉大人,你說(shuō)我怎么就攤上這事酱酬∧⑾眨” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵岳悟,是天一觀的道長(zhǎng)佃迄。 經(jīng)常有香客問(wèn)我,道長(zhǎng)贵少,這世上最難降的妖魔是什么呵俏? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮滔灶,結(jié)果婚禮上普碎,老公的妹妹穿的比我還像新娘。我一直安慰自己录平,他們只是感情好麻车,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著斗这,像睡著了一般动猬。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上表箭,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天赁咙,我揣著相機(jī)與錄音,去河邊找鬼免钻。 笑死彼水,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的极舔。 我是一名探鬼主播凤覆,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼拆魏!你這毒婦竟也來(lái)了盯桦?” 一聲冷哼從身側(cè)響起澡绩,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎俺附,沒(méi)想到半個(gè)月后肥卡,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡事镣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年步鉴,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片璃哟。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡氛琢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出随闪,到底是詐尸還是另有隱情阳似,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布铐伴,位于F島的核電站撮奏,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏当宴。R本人自食惡果不足惜畜吊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望户矢。 院中可真熱鬧玲献,春花似錦、人聲如沸梯浪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)挂洛。三九已至礼预,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間抹锄,已是汗流浹背逆瑞。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留伙单,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓哈肖,卻偏偏與公主長(zhǎng)得像吻育,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子淤井,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 1. this之謎 在JavaScript中布疼,this是當(dāng)前執(zhí)行函數(shù)的上下文摊趾。因?yàn)镴avaScript有4種不同的...
    百里少龍閱讀 988評(píng)論 0 3
  • 由于javascript中的this是在運(yùn)行期綁定的,所以javascript中的this的含義很豐富游两。它可以是全...
    iOneWay閱讀 313評(píng)論 0 0
  • 函數(shù)和對(duì)象 1砾层、函數(shù) 1.1 函數(shù)概述 函數(shù)對(duì)于任何一門(mén)語(yǔ)言來(lái)說(shuō)都是核心的概念。通過(guò)函數(shù)可以封裝任意多條語(yǔ)句贱案,而且...
    道無(wú)虛閱讀 4,525評(píng)論 0 5
  • 首先必須要說(shuō)的是肛炮,this的指向在函數(shù)定義的時(shí)候是確定不了的,只有函數(shù)執(zhí)行的時(shí)候才能確定this到底指向誰(shuí)宝踪,實(shí)際上...
    WERH5知識(shí)分享閱讀 396評(píng)論 0 1
  • js中this這個(gè)關(guān)鍵字侨糟,被自動(dòng)定義在所有函數(shù)的作用域中,先來(lái)看一段簡(jiǎn)單的代碼: 在圖1中所示瘩燥,this關(guān)鍵字則指...
    清風(fēng)不曉明月事閱讀 681評(píng)論 0 3