從幾個題目看一下JavaScript的 共有、私有、靜態屬性和方法 | 運算符優先級等問題

本文主要涉及兩方面的內容:

  • 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行代碼做了什麼

  1. var聲明一個變量getName
  2. function聲明一個函數getName
  3. 給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 Foo.getName();這個括號是函數的括號啊,跟表格裏權重20的括號不一樣,權重20的括號是(1+3)/2(1+3)/2運算中的括號啊!!!

再看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方法
  • 所以輸出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) } 
  1. Foo.getName()調用Foo的靜態方法,輸出3
  2. getName()調用全局變量的getName,輸出5
  3. Foo().getName()直接調用Foo函數,this指向window,this.getName = function(){...}修改一次window中的getName,getName = function(){...}又修改一次window中的getName,輸出2
    在這裏插入圖片描述
  4. getName()window中的getName,輸出2
  5. new Foo.getName()–> new (Foo.getName)()輸出3
  6. new Foo().getName()–>(new Foo()).getName()this.getName = function() {...}是公有方法,輸出1
    如果沒有this.getName = function() {...}這一句,就是輸出4
  7. new 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。
  8. 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”


我盡力了,講的很繁瑣,但是應該講的很明白了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章