本文接着上篇文章繼續和你們聊聊this
,若是是經過上篇文章來到了本篇文章那麼應該對this
有必定的理解了,確定還帶着些對this
的疑惑,下面將講述關於this
的剩下內容,完全的理解this
。若是沒有看過上篇文章的同窗,建議先看上篇文章的內容。java
在講this
以前咱們先了解一個前置知識,簡單賦值,咱們先來看看規範中怎麼定義的:面試
11.13.1 Simple Assignment ( = )數組
The production AssignmentExpression : LeftHandSideExpression = AssignmentExpression is evaluated as follows:數據結構
- Let lref be the result of evaluating LeftHandSideExpression.
- Let rref be the result of evaluating AssignmentExpression.
- Let rval be GetValue(rref).
- Throw a SyntaxError exception if the following conditions are all true:
- Type(lref) is Reference is true
- IsStrictReference(lref) is true
- Type(GetBase(lref)) is Environment Record
- GetReferencedName(lref) is either "eval" or "arguments"
- Call PutValue(lref, rval).
- Return rval.
產生式AssignmentExpression : LeftHandSideExpression = AssignmentExpression
按照下面的過程執行 :app
lref
爲解釋執行LeftHandSideExpression
的結果rref
爲解釋執行AssignmentExpression
的結果rval
爲GetValue(rref)
SyntaxError
異常,當如下條件都成立:Type(lref)
爲Reference
IsStrictReference(lref)
爲true
Type(GetBase(lref))
爲環境記錄項GetReferencedName(lref)
爲"eval
"或"arguments
"PutValue(lref, rval)
rval
對於簡單賦值A=B
,能夠這麼理解:ide
A
,獲得一個引用lrefA
B
,獲得一個值rvalB
rvalB
賦給lrefA
指向的名稱綁定rvalB
上篇文章講到了this
的默認綁定和隱性綁定,默認綁定和隱性綁定中還有幾個容易出錯的場景,咱們先來看看默認綁定:函數
// 實例一
var x = 1;
var obj = {
x: 2,
foo: function() {
console.log(this.x); // 2
function bar() {
console.log(this.x); // 1
}
bar();
}
}
obj.foo();
複製代碼
實例一就是默認綁定的一個很經典的場景,第一個控制檯輸出是2
,這個你們應該都沒有疑問,是隱性綁定,上篇文章已經分析過了。學習
有疑惑的是第二個控制檯輸出爲何是1
,也相信不少人也知道是1
,說這就是默認綁定,函數bar
在函數foo
中調用,函數bar
是獨立調用,bar
沒有帶有任何修飾符的函數調用,因此bar
中this
是指向全局對象window
,因此輸出是1
。對,這種解釋沒毛病,大部分文章都是這麼解釋的,可是這種解釋沒有依據,不能很好的說服我,感受就是在強迫我就是這麼理解的,時間長了,好像是這麼回事。下面從規範中解析爲何第二個輸出是1
,從根本上理解這種默認綁定。ui
前面的文章提到函數調用的時候會建立想要的執行上下文:this
executionContext: {
variable object:vars, functions, arguments
scope chain: variable object + all parents scopes
thisValue: context object
}
複製代碼
每一個執行上下文中都有一個變量對象(Variable object
),變量對象包含了函數形參、函數聲明以及全部的變量聲明。
咱們回到咱們的實例一中來,obj.foo()
調用的時候,會建立函數foo
的執行上下文,函數的變量對象以下:
foo_VO = {
bar: <reference to FunctionDeclaration 'bar'> } 複製代碼
foo
函數調用有變量對象,天然也會有活動對象這個概念,活動對象前面的文章也講過了,咱們來看看ECMAScript
規範是怎麼說的,關於執行上下文、變量對象等概念在ECMAScript5
、ECMAScript6
都沒有說起到,在早版本ECMAScript3.1
中有講解:
10.1.6 Activation Object
When control enters an execution context for function code, an object called the activation object is created and associated with the execution context. The activation object is initialised with a property with name arguments and attributes { DontDelete }. The initial value of this property is the arguments object described below.
當函數調用時建立了執行上下文,活動對象建立並與執行上下文相關聯。活動對象被初始化,並含有一個名爲arguments屬性和不能刪除的屬性,初始化的屬性就是arguments對象。
The activation object is then used as the variable object for the purposes of variable instantiation.
爲了變量實例化,活動對象被用做變量對象。
ECMAScript3.1
中解釋了函數調用的時候,活動對象被用做變量對象。咱們再來看看規範中進入了執行上下文,函數調用是怎麼說的:
10.2.3 Function Code
- The scope chain is initialised to contain the activation object followed by the objects in the scope chain stored in the [[Scope]] property of the Function object.
- Variable instantiation is performed using the activation object as the variable object and using property attributes { DontDelete }.
- The caller provides the this value. If the this value provided by the caller is not an object (including the case where it is null), then the this value is the global object.
咱們只要先看第二點,變量實例化,活動對象被用做了變量對象。這麼說建立執行上下文的時候,有變量對象, 當執行的時候,活動對象被用做變量對象。
咱們再看看函數foo
執行上下文中的活動對象是啥:
foo_AO = {
arguments: {
length: 0
},
bar: <reference to FunctionExpression "bar"> } 複製代碼
在建立函數執行上下文階段,變量對象被建立,變量對象的屬性不能被訪問,此時的函數尚未執行,當函數來到執行階段,變量對象被激活,變成了活動對象,而且裏面的屬性都能訪問到,開始進行執行階段的操做。
因此在實例一中,函數bar()
等價於foo_AO.bar()
,實例一能夠這麼的改寫:
// 實例一改寫
var x = 1;
var obj = {
x: 2,
foo: function() {
console.log(this.x); // 2
function bar() {
console.log(this.x); // 1
}
foo_AO.bar(); // bar()
}
}
obj.foo();
複製代碼
當函數obj.foo()
被調用後,同時函數bar
也會被調用,也就是foo_AO.bar()
被調用,這樣也就回到了咱們熟悉的模式了,函數bar
被foo_AO
修飾了,foo_AO.bar
是屬性訪問,foo_AO.bar
會被解釋執行爲一個Reference
:
foo_AO.bar_reference = {
base: foo_AO,
name: 'bar
}
複製代碼
由於foo_AO.bar
會被解釋執行爲一個Reference
,foo_AO
也是一個對象,因此thisValue = GetBase(foo_AO.bar_reference)
,也就是foo_AO
,然而在ECMAScript
程序中咱們是不能直接接觸到活動對象,只是內部實現,那this
真正的指向是指向哪裏呢,咱們來看看規範(3.1
)中怎麼說的:
10.1.6 Activation Object
The activation object is purely a specification mechanism. It is impossible for an ECMAScript program to access the activation object. It can access members of the activation object, but not the activation object itself. When the call operation is applied to a Reference value whose base object is an activation object, null is used as the this value of the call.
活動對象純粹是一種規範機制。ECMAScript
程序是可能直接訪問活動對象。它能夠訪問活動對象的成員,可是不能訪問活動對象自己。當調用操做應用到其基(base
)值是活動對象的引用(Reference
)時,將null
做爲this
的值。
因此實例一的this
指向null
,最終會不會是null
呢,咱們再來看看下面的:
10.2.3 Function Code
The caller provides the this value. If the this value provided by the caller is not an object (including the case where it is null), then the this value is the global object.
當this
爲null
的是,this
又會指向全局對象,也就是window
。因此實例一中第二個控制檯輸出的this
是執行window
,即this.x = 1
。
講到這裏,把實例一中的第二個控制檯輸出的this
爲何是window
的真正緣由講完了,你們應該能真正的理解是什麼緣由了,也不須要硬記。
// 實例二
function foo() {
console.log(this.a);
}
var obj2 = {
a: 1,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 1
複製代碼
實例二也是隱性綁定的一個經典的應用場景,控制檯最後輸出的值是1
,爲何是1,大部分文章的解釋是在對象屬性引用鏈中只有最後一層在調用位置中起做用,是this
的隱性綁定,在實例二中起做用的是obj2
,因此this
指向的obj2
,即控制檯輸出的是1
,這也是在強行解釋,並無給出合理的解釋緣由。
其實實例二考察的不只僅是this
的指向,還考察了JavaScript
的運算符優先級,若是能把運算符的優先級捋清楚,這種隱性綁定是很好理解的。
obj1.obj2.foo()
執行,其執行順序是怎麼樣的呢,能夠參考JavaScript
運算符的優先級彙總表,優先級最高的是()
,優先級是20
,因此obj1.obj2.foo()
先執行的是函數,因此其函數調用後,產生的MemberExpression
就是obj1.obj2.foo
,那麼這個表達式的執行順序是怎樣的?
看到出來這就是屬性訪問表達式,成員表達式,鏈式成員訪問,其優先級是19
,因此obj1.obj2.foo
後於()
執行,而且成員訪問的執行順序是從左到右的。
即obj1.obj2.foo
是先執行obj1.obj2
獲得執行結果(result
)後再執行結果的result.foo
。這就是obj1.obj2.foo()
的基本執行順序,按照此順序咱們能夠把實例二改寫成:
// 改寫實例二
function foo() {
console.log(this.a);
}
var obj2 = {
a: 1,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
var result = obj1.obj2;
result.foo(); // 1
複製代碼
咱們須要先知道result
的值,result = obj1.obj2
很明顯,這就是一個簡單賦值,上面有介紹簡單賦值。
一開始,聲明瞭變量obj1
和變量obj2
,並都作了簡單賦值,後面又聲明瞭變量result
也作了簡單賦值,都按照上面的簡單賦值的定義,賦值後最終內容以下:
obj1.obj2 = {
a: 1,
foo: function() {
console.log(this.a);
}
}
result = {
a: 1,
foo: function() {
console.log(this.a);
}
}
複製代碼
也就是說對於result = obj1.obj2
,result
的值是一個對象。
而後咱們再來看看result.foo()
的執行,也就是等價於:
result = {
a: 1,
foo: function() {
console.log(this.a);
}
}
reslut.foo();
複製代碼
這種方式也就是咱們上篇文章分析過的方式,result.foo
執行解釋結果是一個Reference
,其數據結構是:
reference_result_foo = {
base: result,
name: "foo"
}
複製代碼
這裏的this
的最終指向就是result
,就再也不具體分析了,參考上篇文章的內容,this.a
等價於result.a
,控制檯輸出的也就是1
。
咱們再回到實例二中,
// 實例二
function foo() {
console.log(this.a);
}
var obj2 = {
a: 1,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 1
複製代碼
這裏的this
指向也就是obj2
,控制檯輸出的也就是1
了。
還有一個很常見的場景:
// 實例三
function foo() {
console.log(this.a); // 1
}
var obj = {
a: 2,
foo: foo
};
var bar = obj.foo;
var a = 1;
bar();
複製代碼
這裏控制檯輸出是1
,大部分解釋是this
綁定隱式丟失,而後指向了全局,因此是1
。這種解釋也有點牽強。
其實仍是能夠從簡單賦值的角度來解釋,bar
的值是obj.foo
的值,是obj.foo
的真正的值,是經過GetValue()
方法獲得的值,不是引用,這裏也就是函數foo
,並非對象obj
的方法foo
。若是這個能理解的話,這裏很好解釋了,執行函數bar
其實就是執行函數foo
,this
指向也就是全局對象window
,this.a
也就是window.a
,輸出的值也就是1
了。具體的緣由就不展開解釋了,都已經寫到這裏了,應該很容易理解了。這也就是所謂的隱式丟失。
在函數原型上定義了三個方法容許手動設置函數調用的this
值:apply()
、call()
和bind()
,這個三個方法做用是一致的,強行修改函數調用的this
值。咱們下面來看看這三個方法:
15.3.4.3 Function.prototype.apply (thisArg, argArray)
When the apply method is called on an object func with arguments thisArg and argArray, the following steps are taken:
- If IsCallable(func) is false, then throw a TypeError exception.
- If argArray is null or undefined, then
a. Return the result of calling the [[Call]] internal method of func, providing thisArg as the this value and an empty list of arguments.
- ...
- Return the result of calling the [[Call]] internal method of func, providing thisArg as the this value and argList as the list of arguments.
NOTE The thisArg value is passed without modification as the this value. This is a change from Edition 3, where a undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value.
我只把apply()
調用的部分步驟列出來了,只須要看第二步和第三步就能夠了。
當以thisArg
和argArray
爲參數在一個func
對象上調用apply
方法,採用以下步驟:
IsCallable(func)
是false
, 則拋出一個TypeError
異常argArray
是null
或undefined
, 則thisArg
做爲this
值並以空參數列表調用 func
的[[Call]]
內部方法的結果thisArg
做爲this
值並以argList
做爲參數列表,調用func
的[[Call]]
內部方法,返回結果 規範把apply
方法的調用寫的明明白白的,傳了兩個參數,一是thisArg
,最終會把thisArg
做爲this
值,二是argArray
,是一個數組,做爲參數列表,最後調用func
的[[Call]]
內部方法,返回結果。咱們來看看下面的實例:
// 實例四
function foo(param){
console.log(this.a); // 10
console.log(param); // 1
}
var obj = {
a : 10
};
foo.apply(obj, [1]);
複製代碼
實例四中函數foo
調用後this
強行被指向了obj
。
須要注意的是,thisArg
是undefined
或null
時它會被替換成全局對象,全部其餘值會被應用ToObject
並將結果做爲this
值。參考實例五:
// 實例五
function foo(param){
console.log(this.a); // 2
console.log(param); // 1
}
var obj = {
a : 10
};
var a = 2;
foo.apply(null, [1]);
複製代碼
15.3.4.4 Function.prototype.call(thisArg[ , arg1 [ , arg2, … ]])
When the call method is called on an object func with argument thisArg and optional arguments arg1, arg2 etc, the following steps are taken:
- If IsCallable(func) is false, then throw a TypeError exception.
- t argList be an empty List.
- If this method was called with more than one argument then in left to right order starting with arg1 append each argument as the last element of argList
- Return the result of calling the [[Call]] internal method of func, providing thisArg as the this value and argList as the list of arguments.
NOTE The thisArg value is passed without modification as the this value. This is a change from Edition 3, where a undefined or null thisArg is replaced with the global object and ToObject is applied to all other values and that result is passed as the this value.
當以thisArg
和可選的arg1
,arg2
等等做爲參數在一個func
對象上調用call
方法,採用以下步驟:
IsCallable(func)
是false
, 則拋出一個TypeError
異常argList
爲一個空列表arg1
開始以從左到右的順序將每一個參數插入爲argList
的最後一個元素thisArg
做爲this
值並以argList
做爲參數列表,調用func
的[[Call]]
內部方法,返回結果 一樣規範把call
方法的調用寫的明明白白的,能夠接收多個參數,第一個參數是thisArg
,最終會做爲this
值,從第二個參數開始全部的參數都是func
的參數列表,調用func
的[[Call]]
內部方法,返回結果。咱們來看看下面的實例:
// 實例六
function foo(param){
console.log(this.a); // 10
console.log(param); // 1
}
var obj = {
a : 10
};
foo.call(obj, 1);
複製代碼
實例六中函數foo
調用後this
強行被指向了obj
。
須要注意的是,thisArg
是undefined
或null
時它會被替換成全局對象,全部其餘值會被應用ToObject
並將結果做爲this
值。
15.3.4.5 Function.prototype.bind (thisArg [, arg1 [, arg2, …]])
The bind method takes one or more arguments, thisArg and (optionally) arg1, arg2, etc, and returns a new function object by performing the following steps:
- Let Target be the this value.
- If IsCallable(Target) is false, throw a TypeError exception.
- Let A be a new (possibly empty) internal list of all of the argument values provided after thisArg (arg1, arg2 etc), in order.
- Let F be a new native ECMAScript object
- ...
- Return F
只把bind()
執行的部分步驟列出來了,只要看上面幾個步驟就能夠了。
bind
方法須要一個或更多參數,thisArg
和(可選的)arg1
, arg2
, 等等,執行以下步驟返回一個新函數對象:
Target
爲this
值IsCallable(Target)
是false
, 拋出一個TypeError
異常A
爲一個(可能爲空的)新內部列表,它包含按順序的thisArg
後面的全部參數(arg1
, arg2
等等)F
爲一個新原生ECMAScript
對象F
一樣規範也把bind
方法的調用寫的明明白白的,bind
最終返回的是一個新的函數對象,這個和上面的apply
方法和call
方法的調用不同。
將值綁定到了函數的this
上,並將綁定好的函數返回,因此bind
只是一個函數,不會馬上執行。咱們來看看下面的實例:
// 實例七
function foo(param){
console.log(this.a); // 10
console.log(param); // 1
}
var obj = {
a : 10
};
var foo = foo.bind(obj, 1);
foo();
複製代碼
實例七中函數foo
調用後this
強行被指向了obj
。
上面就是關於顯示綁定的全部內容,主要是經過函數原型上的apply
、call
和bind
三個方法實現的,面試中對這三個方法也是常常會問到,這三個方法原生實現怎麼寫,能夠參考規範中對這三個方法的執行步驟,在這裏暫時不展開了,後期考慮。
咱們先看個實例:
// 實例八
function Foo() {
this.x = 10;
}
var foo = new Foo();
console.log(foo.x); // 10
複製代碼
對於輸出是10
,緣由你們都知道,咱們來看看new
具體作來什麼,使得Foo
函數中的this
指向了foo
:
11.2.2 The new Operator
The production NewExpression : new NewExpression is evaluated as follows:
- Let ref be the result of evaluating NewExpression.
- Let constructor be GetValue(ref).
- If Type(constructor) is not Object, throw a TypeError exception
- If constructor does not implement the [[Construct]] internal method, throw a TypeError exception
- Return the result of calling the [[Construct]] internal method on constructor, providing no arguments (that is, an empty list of arguments)
產生式NewExpression : new NewExpression
按照下面的過程執行:
ref
爲解釋執行NewExpression
的結果constructor
爲GetValue(ref)
Type(constructor)
不是Object
,拋出一個TypeError
異常constructor
沒有實現[[Construct]]
內置方法 ,拋出一個TypeError
異常constructor
的[[Construct]]
內置方法的結果 , 傳入按無參數傳入參數列表 ( 就是一個空的參數列表 ) 按照規範中的定義,就實例八而言,ref
最終解釋結果是一個引用(reference
),constructor
爲GetValue(ref)
,也就是函數Foo
:
constructor = function Foo() {
this.x = 10;
}
複製代碼
最後是返回調用constructor
的[[Construct]]
內置方法的結果。咱們再來看看[[Construct]]
內置方法是怎麼執行的:
13.2.2 [[Construct]]
When the [[Construct]] internal method for a Function object F is called with a possibly empty list of arguments, the following steps are taken:
- Let obj be a newly created native ECMAScript object
- Set all the internal methods of obj as specified in 8.12
- Set the [[Class]] internal property of obj to "Object"
- Set the [[Extensible]] internal property of obj to true
- Let proto be the value of calling the [[Get]] internal property of F with argument "prototype"
- If Type(proto) is Object, set the [[Prototype]] internal property of obj to proto.
- Type(proto) is not Object, set the [[Prototype]] internal property of obj to the standard built-in Object prototype object as described in 15.2.4
- Let result be the result of calling the [[Call]] internal property of F, providing obj as the this value and providing the argument list passed into [[Construct]] as args
- If Type(result) is Object then return result
- Return obj
當以一個可能的空的參數列表調用函數對象F
的[[Construct]]
內部方法,採用如下步驟
obj
爲新建立的ECMAScript
原生對象8.12
設定obj
的全部內部方法obj
的[[Class]]
內部方法爲"Object
"obj
的[[Extensible]]
內部方法爲true
proto
爲以參數"prototype
"調用F
的[[Get]]
內部屬性的值Type(proto)
是Object
,設定obj
的[[Prototype]]
內部屬性爲proto
Type(proto)
不是Object
,設定obj
的[[Prototype]]
內部屬性爲15.2.4
描述的標準內置的Object
的prototype
對象obj
爲this
值,調用[[Construct]]
的參數列表爲args
,調用F
的 [[Call]]
內部屬性,令result
爲調用結果Type(result)
是Object
,則返回result
obj
函數對象F
的[[Construct]]
作了this
指向的綁定,內部建立了一個新的對象obj
,新對象內置原型屬性指向了F
的原型對象prototype
,最後新建立的對象obj
爲this
的值,並調用F
的[[Call]]
內部屬性,最後返回obj
。上面的實現步驟能夠簡單的理解爲下面程序:
var foo = new Foo() = {
var obj = {};
obj.__proto__ = Foo.prototype;
var result = Foo.call(obj);
return typeof result === 'Object' ? result : obj;
}
複製代碼
上面也就是面試常常問到的手動實現new
原理,能夠作爲參考。因此當執行var foo = new Foo()
時,this
就指向新的對象,最後把執行結果返回給了foo
,foo
真實的值也就是對象。
也就是說函數Foo
中this
指向的是foo
,因此控制檯輸出的值是foo.x
,也就是10
了。
箭頭函數是ECMAScript6
提出來的,在這裏不對箭頭函數作詳細的介紹,只說明箭頭函數關於this
的狀況。
在ECMAScript6
中有這麼一句話:
8.1.1.3Function Environment Records
A function Environment Record is a declarative Environment Record that is used to represent the top-level scope of a function and, if the function is not an ArrowFunction, provides a this binding.
函數環境記錄是聲明式環境記錄,用於表示函數的頂級範圍。若是函數不是箭頭函數,那麼會提供this
綁定。也就是說在箭頭函數中不存在this
綁定這個說法。
[[thisBindingStatus]]
If the value is "lexical", this is an ArrowFunction and does not have a local this value.
當值是lexical時,表示是箭頭函數而且沒有本身的this值綁定
9.2.4FunctionInitialize (F, kind, ParameterList, Body, Scope)
- If kind is Arrow, set the [[ThisMode]] internal slot of F to lexical. 當箭頭函數初始化時,
[[ThisMode]]
設置爲lexical
[[ThisMode]]
lexical means that this refers to the this value of a lexically enclosing function.
當[[ThisMode]]
值是lexical
時,表示this
值是當前封閉函數lexical scope
。
9.2.1.2OrdinaryCallBindThis ( F, calleeContext, thisArgument )
- If thisMode is lexical, return NormalCompletion(undefined)
- Return envRec.BindThisValue(thisValue)
在函數執行前綁定this
的時候,傳入的thisArgument
會被直接忽略。
再來看一段話:
14.2.16 Runtime Semantics: Evaluation
An ArrowFunction does not define local bindings for arguments, super, this, or new.target. Any reference to arguments, super, this, or new.target within an ArrowFunction must resolve to a binding in a lexically enclosing environment. Typically this will be the Function Environment of an immediately enclosing function.
箭頭函數不會爲arguments
、super
、this
或者new.target
定義本地綁定。對於箭頭函數中的arguments
、super
、this
或者new.target
的任何引用都必須解析爲lexically
封閉環境中綁定。一般這將是一個當即封閉的函數的函數環境。
總之,箭頭函數初始化,在lexical
環境中,沒有自身的this
綁定,箭頭函數也無法修改this
,函數內的this
對象,就是定義時所在的對象,而不是使用時所在的對象。
咱們來看看下面的實例:
// 實例九
function foo() {
setTimeout(() => {
console.log(this.a); // 2
}, 100);
}
var a = 1;
foo.call({ a: 2 });
複製代碼
實例九中控制檯輸出的是2
,不是1
,若是定時器裏面的函數是普通函數的話,那麼this
的指向會是window
,輸出的值1
。在這裏輸出的是2
。
再來看看一個實例,有點意思:
// 實例十
var obj= {
that: this,
bar: function() {
return () => {
console.log(this);
}
},
baz: () => {
console.log(this);
},
bam: function() {
console.log(this);
}
}
console.log(obj.that); // window
obj.bar()(); // obj
obj.baz(); // window
obj.bam(); // obj
複製代碼
看到實例九,應該有人會對結果感到奇怪,這個能夠思考下。
obj
的當前做用域是window
,obj.that === window
function
(function
有本身的函數做用域)將其包裹起來,this
會指向window
,如上面的baz
函數function
包裹的目的就是將箭頭函數綁定到當前的對象上。函數的做用域是當前這個對象,而後箭頭函數會自動指向函數所在做用域的this
,即obj
。把箭頭函數的相關定義理解清楚,就不會存在疑惑了。
到這裏已經把關於this
的內容幾乎所有梳理完了。
文章若有不正確的地方歡迎各位大佬指正,也但願有幸看到文章的同窗也有收穫,一塊兒成長!
-------------------------------本文首發於我的公衆號----------------------------