例題
如題:
答案解析
function A() {
B = function () {console.log(10)}
return this
};
A.B = function () {console.log(20)};
A.prototype.B = function () {console.log(30)}
var B = function () {console.log(40)}
function B() {console.log(50)}
A.B() // answer 20 【原型與原型鏈】
// 在`A`的原型物件中查詢是否有`B`函式並且呼叫,這裡並未執行`A`函式。
// A.B = function () {console.log(20)};
// 在A的原型物件中新增了`B`函式,停止查詢,所以答案為 20
B() // answer 40 【函式表示式和函式宣告】
// var B = function () {console.log(40)}
// function B() {console.log(50)}
// 同名 -> 函式提升會 > 變數提升
// 換言之 同名的函式表示式和函式宣告同時存在時 總是執行表示式
A().B() // answer 10 【函式表示式和函式宣告】
// A() 執行函式A ==> 1.變數B重新賦值函式 2.返回this(window)
// .B() 執行全域性下的B函式 已經被重新賦值 所以輸出10
B() // answer 10
// 上面的程式碼執行過A函式了,此時全域性下的B函式輸出10
new A.B() // answer 20【函式表示式和函式宣告】
// new 執行了 A.B = function () {console.log(20)};
new A().B() // answer 30
// new 執行建構函式 A ,全域性變數 B 重新賦值函式10
// 此時A() 指標指向哪裡?
// 首先要知道 new 做了什麼事?
// ==> 建立空物件objA objA.__proto__ = A.prototype
// .B() 在A的原型物件中查詢 B; A.prototype 指向函式的原型物件
// A.prototype.B = function () {console.log(30)} 輸出 30
複製程式碼
原型和原型鏈
prototype
每一個函式都有一個
prototype
屬性。
function Foo() {}
Foo.prototype; // {constructor,__proto__}
複製程式碼
無論什麼時候,只要建立了一個新函式,根據一組特定的規則為該函式建立一個prototype 屬性,這個屬性指向函式的原型物件。
那麼這個建立的原型物件是什麼呢?
{
constructor: ƒ Foo(),
__proto__: Object
}
複製程式碼
constructor 屬性
每一個原型物件都有一個
constructor
屬性
建立了自定義的建構函式後,其原型物件只會預設取得 constructor
屬性。這個屬性解決了物件識別問題,即可以通過該屬性判斷出例項是由哪個建構函式建立的。
Foo.prototype.constructor === Foo; // true
複製程式碼
前面說了,原型物件只會預設取得 constructor
屬性,那麼原型物件的其他屬性(比如:__proto__
)是這麼來的呢,這就要說到 __proto__
指標了。
proto
每一個例項都有一個
__proto__
指標,指向建構函式的原型物件。
var foo = new Foo();
foo.__proto__ === Foo.prototype; //true
複製程式碼
上面提到的建構函式的原型物件它本身也是一個例項,所以在它內部會有一個__proto__
指標。
建構函式(補充)
ECMAScript
中提供了建構函式來建立新物件。但建構函式本身就是一個函式,與普通函式沒有任何區別,只不過為了區分,一般將其首字母大寫,但這並不是必須的。
函式被 new 關鍵字呼叫時就是建構函式。
function f(name) {
console.log("execute");
this.name = name;
}
var k = new f("k"); // execute =====> 呼叫new
console.log(k); // {name: "k"}
var h = f("h"); // execute =====> 未呼叫new
console.log(h); // undefined
複製程式碼
從上面程式碼可以看出:
- 首字母是否大寫並不影響函式 f 作為建構函式使用,
- 不使用 new 呼叫函式就是普通函式,直接執行內部程式碼,使用
new
,函式的角色就成為了建構函式,建立一個物件並返回。
物件由建構函式通過 new 創造物件的步驟
var obj = {}; // 建立一個空物件
obj.__proto__ = constructor.prototype;//新增__proto__屬性,並指向建構函式的prototype 屬性。
constructor.call(this); // 繫結this
return obj;
複製程式碼
new 關鍵字的內部實現機制:
- 建立一個新物件;
- 將建構函式的作用域賦值給新物件;
- 執行建構函式中的程式碼;
- 返回新物件
原型鏈
原型鏈的理論主要基於上述提到的建構函式、例項和原型的關係:
- 每一個建構函式都有一個原型物件
- 原型物件都包含一個指向建構函式的
constructor
屬性 - 每一個例項都包含一個指向原型物件的
__proto__
指標 其中最最重要的是第三條,依賴這條關係,層層遞進,就形成了例項與原型的鏈條。
接著上面的探索,建構函式的原型的原型是由 Object
生成的,那麼 Object
的原型是由什麼生成?而原型鏈的終點又是在哪?
Object.prototype.__proto__ // null
null.__proto__; // Uncaught TypeError: Cannot read property '__proto__' of null
// game over
複製程式碼
原型的終點是 null
,因為 null
沒有 proto
屬性。
最後以一個例子來理解上面所談到的原型與原型鏈
function Foo(){} // 建構函式 Foo
var foo = new Foo() // foo.__proto__ 指向 Foo.prototype
複製程式碼
函式宣告、函式表示式
函式宣告 function name(){}
函式宣告是用指定的引數宣告一個函式。一個被函式宣告建立的函式是一個 Function
物件,具有 Function
物件的所有屬性、方法和行為。
// 函式宣告語法
function name([param[, param[, ... param]]]) { statements }
複製程式碼
函式表示式 var name = function(){}
在函式表示式中我們可以忽略函式名稱建立匿名函式,並將該匿名函式賦值給變數。
var add = function(a, b) {
return a + b;
};
add(2, 3) // 5
複製程式碼
當然, 也可以建立命名函式表示式 Named function expression:
var add = function func(a, b) {
return a + b;
};
add(2, 3) // 5
複製程式碼
命名函式表示式中函式名稱只能作為函式體作用域內的區域性變數,外部不可訪問。
var a = function pp(v) {
v++;
if (v>3) {
return v;
} else {
return pp(v);
}
}
a(1); // 4
pp(1); // ReferenceError: pp is not defined
複製程式碼
函式宣告提升
對於函式宣告建立的函式,可以在本作用域內任意位置訪問。
a(); // 1
function a() {
return 1;
}
a(); // 1
複製程式碼
而函式表示式不會。
console.log(a); // undefined (只是變數提升)
a(1); // TypeError: a is not a function
var a = function(v) {
console.log(v);
};
複製程式碼
函式提升和變數提升的疑惑分析
console.log(fn); // [Function: fn]
var fn = function () {
console.log(1);
}
function fn() {
console.log(2);
}
fn() // 1
複製程式碼
提升過程
// 函式提升
function fn() {
console.log(2);
}
// 變數提升
var fn;
fn = function () {
console.log(1);
}
fn() //最終輸出1
複製程式碼