本文主要涉及兩方面的內容:
- JavaScript的公有、私有、靜態屬性和方法
- 運算符的優先級
文章目錄
開胃菜
輸出什麼?
function A(x) {
this.x = x;
}
A.prototype.x = 1;
function B(x) {
this.x = x;
}
B.prototype = new A();
var a = new A(2),
b = new B(3);
delete b.x;
console.log(a.x, b.x)
答案是 2 undefined
(❗️提示:下邊說的實例化、公有屬性,你聽不懂沒關係,這個文章就是解釋這些的。但是原型啊什麼的你聽不懂,你就按照我的提示去補充知識吧。👻)
爲什麼a.x輸出2
我們先看一下a本身的構造。
a是A的實例,那麼a的隱式原型就指向A的顯式原型,也就是說A的prototype
和a的__proto__
相等。輸出一下,確實是相同的。(這裏看不懂就去補原型的知識)
那這個題的A就是這樣的:
A.prototype.x = 1;
是給A的原型上添加一個公有屬性。this.x = x;
是A函數體裏邊的公有屬性。
當函數體本身有某個方法或者屬性的時候,就會把原型鏈裏邊的遮蓋掉。如果函數體中沒有某個方法或者屬性,就回去原型鏈中找。
題目裏實例化的對象直接使用.
運算符,是直接訪問A函數體裏邊的公有屬性,而A函數體中有個x了,所以不會用到原型鏈裏的x。想要訪問到原型鏈裏的x只能去原型鏈裏找。
function A(x) {
this.x = x;
}
A.prototype.x = 1
var a = new A(2)
console.log(a.x)
console.log(a.__proto__.x)
函數體中沒有x的時候,就直接使用原型鏈裏邊的了。
function A(x) {
// this.x = x;
}
A.prototype.x = 1
var a = new A(2)
console.log(a.x)
console.log(a.__proto__.x)
爲什麼b.x輸出undefined
先分析一下B的代碼:
function B(x) {
this.x = x;
}
B.prototype = new A();
創建一個函數B,B.prototype = new A();
這句話就是B.prototype
是A的實例,也就是說B.prototype
的隱式原型就是A的顯式原型。
那對於var b = new B(3);
現在的邏輯就是實例b用.
運算符訪問x,就是訪問b中的共有屬性x:
B
函數體中有x,那就訪問x
B
函數體中沒有x,那就訪問B.prototype.x
B.prototype
中沒有x,那就訪問B.prototype__proto__.x
捋清楚訪問的邏輯了吧,那現在我們就來看一下輸出什麼:
function A(x) {
this.x = x;
}
A.prototype.x = 1;
function B(x) {
this.x = x;
}
B.prototype = new A();
var b = new B(3);
delete b.x;
console.log(b.x)
因爲B函數體中有x,實例化b = new B(3)
之後,b是B的實例,此時B函數體中的x = 3 。
b.x指的是B函數體中的x。
delete b.x;
將函數體中的x刪除了。此時b.x指的就是b原型鏈上的b了,也就是先看看B.prototype
中有沒有x。
那我們看看b的構造:b__proto__
中也就是B.prototype
中有個x!並且還是undefined,找到x了,直接輸出undefined即可。
undefined哪裏來的?
B.prototype = new A();
B的顯式原型是A是實例對象,A函數體中有x,B.prototype
實例化的時候沒有攜帶參數,也就是沒給A的x賦值,所以輸出undefined。
不信你就試一下,給B.prototype = new A();
加上個值。
進入正題
剛纔我一直在說共有屬性公有屬性,那還有私有的?
其實不僅有私有的,還有靜態的嗷。
function User(id) {
this.id = id; //公有屬性
var id = id; //私有屬性
function getId() { //私有方法
console.log(this.id)
}
}
User.prototype.getId = function() { //公有方法
console.log(this.id)
}
User.id = 'Sian靜態'; //靜態屬性
User.getId = function() { //靜態方法
console.log(this.id)
}
var sian = new User('SianOvO'); //實例化
- 調用公有方法、公有屬性,需要先實例化對象,也就是用new操作符,公有方法不能調用私有方法和靜態方法的
console.log(sian.id) //輸出SianOvO sian.getId() //輸出SianOvO
- 調用靜態方法、靜態屬性,無需實例化
console.log(User.id) //輸出Sian靜態 User.getId() //輸出Sian靜態
- 私有方法、私有屬性外部是不可以訪問的
看懂了就做個題
看到剛纔那裏你可能覺得自己又懂了,那做個題吧。👏
請寫出以下輸出結果:
function Foo() {
getName = function () { console.log(1); };
return this;
}
Foo.getName = function () { console.log(2);};
Foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log(4);};
function getName() { console.log(5);}
//請寫出以下輸出結果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
直接告訴你答案:2 4 1 1 2 3 3
這個題綜合了很多知識,看了答案不懂也沒事,往下看解析。
整理代碼
拿到這段代碼之後,要進行預處理:變量提升(hoisting),先function聲明的函數,再var聲明的變量,所以提升以後代碼邏輯如下:
var getName = undefined
function Foo() {
getName = function () { console.log(1) }
return this
}
function getName() { console.log(5) }
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
getName = function () { console.log(4) }
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
看一下上邊的5,6,10行代碼做了什麼:
- var聲明一個變量getName
- function聲明一個函數getName
- 給getName賦值,把原來function聲明的函數覆蓋掉
所以覆蓋之後代碼邏輯如下:
var getName = undefined
function Foo() {
getName = function () { console.log(1) }
return this
}
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
getName = function () { console.log(4) }
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
思路分析
整理完代碼邏輯可以做題啦
第1問Foo.getName();
沒實例化,直接使用構造函數 + .
運算符,這是訪問靜態方法。
執行過程:
- 執行Foo的靜態方法:
Foo.getName = function () { console.log(2) }
- 所以輸出2
第2問getName();
執行過程:
- 調用全局的getName方法
var getName = function () { console.log(4) }
。 - 所以輸出4
第3問Foo().getName();
先看一下Foo()
函數:
getName = function () { console.log(1) }
的作用就是給getName賦值一個函數。Foo()
函數體中沒有聲明getName變量,因此往上一層作用域中找,找到了window,於是更改全局作用域(window)中的getName方法。- 函數直接調用,this指向window。
return this
就是返回window。
執行過程:
- 執行
Foo()
修改getName方法並將this指向window - 調用window的getName方法
function () { console.log(4) }
- 所以輸出1
第4問getName();
跟上一題一樣,就是window.getName();
執行過程:
- 調用window的getName方法
function () { console.log(4) }
- 所以輸出1
第5問new Foo.getName();
一頭霧水?這裏是考察的是JS的運算符優先級問題。放個優先級表格(完整版戳👉MDN web docs 運算符優先級)
本題中涉及到的運算符:
- 19
- 成員訪問
… . …
- new (帶參數)
new … ( … )
- 函數調用
… ( … )
- 成員訪問
- 18
- new(無參數)
new …
- new(無參數)
注意new Foo.getName();
這個括號是函數的括號啊,跟表格裏權重20的括號不一樣,權重20的括號是運算中的括號啊!!!
再看new Foo.getName();
該怎麼解析:
解析到new
之後,繼續往後解析,看看需要new什麼東西,也就是new Foo
。但是Foo
後邊卻有個.
運算符。此時就是沒有new無參數列表
(18)和.
(19)比較,.
的優先級高,所以先執行Foo.getName
。那執行順序就變爲下圖:
此時(Foo.getName)
後邊有個()
,此時new無參數
(18)就變成了new有參數
(19),執行帶參數列表的new。
再次提醒,這個()
括號是調用函數時的括號,跟(Foo.getName)
的運算符括號不同。
執行過程:
Foo.getName
就是Foo.getName = function () { console.log(2) }
new Foo.getName();
就是把function () { console.log(2) }
當作構造函數,創建其實例對象。- 因此輸出2
Q:爲什麼有輸出啊?我也沒調用函數吧?
A:如果你有這個疑問,建議看一哈JavaScript中 構造函數的new都做了什麼。new的第三步就是執行構造函數中的代碼,因此不需要你調用,new 的過程中它自己就執行啦。
第6問new Foo().getName();
第五問搞明白之後。剩下的就好懂多了。
new ()
和.
的優先級是一樣的,從左往右執行,所以先執行new Foo()
,再次修改全局中的getName,然後返回一個this,這個this是指向new Foo()
這個實例化對象的。然後執行實例化對象的getName方法。
注意!這個看似跟第3問一樣,其實不一樣,第三問中是直接調用Foo(),但是本問中進行了new實例化操作。
執行過程:
- 創建Foo的實例對象,this指向實例對象
- 調用實例對象的getName方法(實例對象能調用構造函數的公有方法和屬性)
- 先看函數體中有沒有getName方法,函數體中沒有
getName
方法,需要去原型鏈中尋找。
雖然我們看代碼可以看到函數體有個getName ,但是那個getName是私有方法!對於代碼執行來說外部是訪問不到的。 - 實例的原型鏈有找到了,Foo的prototype中有getName方法
Foo.prototype.getName = function () { console.log(3) }
,因此執行getName方法
- 先看函數體中有沒有getName方法,函數體中沒有
- 所以輸出3。
第7問new new Foo().getName();
遇到new
,繼續向右解析,又遇到new
,繼續向右解析,遇到()
和.
,new()
優先級等於.
,執行new()
。下圖左邊是一個new,沒參數列表,右邊是帶參數列表,執行右邊的new。
執行過程:
let 實例 = new Foo()
指針指向實例let x = 實例.getName
x 就是ƒ () { console.log(3) }
new x()
就是將x實例化,實例化的過程中自動調用ƒ () { console.log(3) }
- 所以輸出3
再來個題自己測驗一下
function Foo() {
this.getName = function() {
console.log(1)
return { getName: getName }
}
getName = function() { console.log(2) }
return this
}
Foo.getName = function() { console.log(3) }
Foo.prototype.getName = function() { console.log(4) }
var getName = function() { console.log(5) }
function getName() { console.log(6) }
Foo.getName()
getName()
Foo().getName()
getName()
new Foo.getName()
new Foo().getName()
new Foo().getName().getName()
new new Foo().getName()
不詳細將了,大致說一下思路。
預處理之後的代碼:
var getName = undefined
function Foo() {
this.getName = function() {
console.log(1)
return { getName: getName }
}
getName = function() { console.log(2) }
return this
}
Foo.getName = function() { console.log(3) }
Foo.prototype.getName = function() { console.log(4) }
getName = function() { console.log(5) }
Foo.getName()
調用Foo的靜態方法,輸出3getName()
調用全局變量的getName,輸出5Foo().getName()
直接調用Foo函數,this指向window,this.getName = function(){...}
修改一次window中的getName,getName = function(){...}
又修改一次window中的getName,輸出2
getName()
window中的getName,輸出2new Foo.getName()
–>new (Foo.getName)()
,輸出3new Foo().getName()
–>(new Foo()).getName()
,this.getName = function() {...}
是公有方法,輸出1。
如果沒有this.getName = function() {...}
這一句,就是輸出4new Foo().getName().getName()
–>((new Foo()).getName()).getName()
到這裏((new Foo()).getName())
和第6問一模一樣,但是後邊又加了個.getName() ,這個.getName()
是誰的?- 第6問調用的公有方法
this.getName = function() {...}
,裏邊還有return { getName }
,返回了一個對象,並且給對象的getName屬性賦值一個getName方法 - 但是
this.getName = function() {...}
裏邊沒getName方法,往上一層找,找到Foo裏邊的getName = function() { console.log(2) }
,輸出2 - 如果你把
getName = function() { console.log(2) }
這句註釋掉。那就是this.getName裏邊沒getName方法,往上一層找Foo裏邊也沒有,繼續往外找,在全局作用域中找到了,輸出5。
- 第6問調用的公有方法
new new Foo().getName()
–>new ((new Foo()).getName())
((new Foo()).getName())
就是把this.getName = function() {...}
當構造函數實例化,new的過程中執行代碼,輸出1
最後關於裏邊的返回值補充一個小題目
function Car() {
this.make = "Lamborghini"
return { make: "Maserati" }
}
const myCar = new Car()
console.log(myCar.make)
輸出Maserati,myCar是Car的實例,但是Car有個返回值是返回到一個對象,所以myCar現在是這個對象的實例。輸出的就是該對象的name “Maserati”
我盡力了,講的很繁瑣,但是應該講的很明白了