1.函數也是對象
有時我們會好奇為什么能給一個函數添加屬性,函數難道不應該就是一個執行過程的作用域嗎?
var fn=function(){
this.name='手機'
}
fn.age=11
console.log(fn.age) //11
console.log(fn)
// function(){
// this.name='手機'
// }
其實,在JS里,函數就是一個對象,所有的函數都是Function的實例.即
console.log(a.__proto__==Function.prototype) //true
不明白上面的代碼是什么意思?咱們先往下看。
2.構造函數和普通函數的區別
function fn(){
this.name='手機'
console.log(this)
}
fn()
//此時這里面的this表示window
var m=new fn()
//此時this表示m對象。
- 用關鍵詞new 調用的函數叫做構造函數
- 雖然沒有硬性規定,為了和普通函數區分,構造函數一般首字母大寫。
3.構造函數和實例的關系
function Person(name) {
this.name = name
}
let p = new Person('Tom');
console.log(p) //{name:'Tom'}
上面代碼,我們通過new
關鍵字調用Person
生成的對象p就是構造函數的實例。
現在我們修改代碼:
<!-- demo1 -->
function Person(name) {
this.name = name
return {}
}
let p = new Person('Tom');
console.log(p) //{}
<!-- demo2 -->
function Person(name) {
this.name = name
return name;
}
let p = new Person('Tom');
console.log(p) //{name:'Tom'}
從上面代碼我們可以看出兩次生成的實例并不一樣。這是為什么?
其實,構造函數不需要顯示的返回值。使用new來創建對象 (調用構造函數) 時,
- 如果
不返回值
,實例為生成的新對象 - 如果返回
number、string、boolean、symbol類型、undefined、null
,會忽略返回值,實例為生成的新對象 - 如果返回
對象、數組、函數
,實例為為返回的對象
4.函數和原型的關系
function Fn(name){
this.name=name
this.kind='電子產品'
}
var m1=new Fn('手機');
var m2=new Fn('電腦');
結果會生成兩個m1,m2對象。
m1={
name:'手機',
kind:'電子產品'
}
m2={
name:'電腦',
kind:'電子產品'
}
//每一個實例對象,都有自己的屬性和方法的副本。修改任何一個都不會影響另一個。
這不僅無法做到數據共享,也是極大的資源浪費。
function Fn(name){
this.name=name
}
Fn.prototype = { kind : '電子產品' };
var m1=new Fn('手機');
var m2=new Fn('電腦');
結果會生成兩個m1,m2對象。
m1={
name:'手機',
kind:'電子產品'
}
m2={
name:'電腦',
kind:'電子產品'
}
//kind屬性放在prototype對象里,是兩個實例對象共享的。只要修改了prototype對象,
就會同時影響到兩個實例對象。
- JS里,我們創建的每一個函數都有一個prototype(原型)屬性,這個屬性指一個——用于包含該對象所有實例的共享屬性和方法——的對象。
- 原型對象同時包含一個指針指向這個這個函數,這個指針就是constructor,這個函數也就是構造函數。
了解了原型和constructor指針后,我們通過一個例子以及一張圖來進一步了解這兩者的關系。
function Person(){
this.age=10
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 24;
Person.prototype.sayAge = function () {
alert(this.age);
};
console.log(Person.prototype.constructor===Person); //true
5.原型和實例的關系
function Person(){
this.age=10
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 24;
Person.prototype.sayAge = function () {
alert(this.age);
};
//實例
var person1 = new Person('Lee');
var person2 = new Person('Lucy');
我們新建了兩個實例person1和person2,這些實例的內部都會包含一個指向其構造函數的原型對象的指針(內部屬性),不是對外訪問的API,這個指針叫[[Prototype]],在ES5的標準上沒有規定訪問這個屬性,所以在瀏覽器上是不能console出這個屬性的,會報錯。但是大部分瀏覽器通過__proto__的屬性來訪問它,成為了實際的通用屬性,于是在ES6的附錄里寫進了該屬性。
prototype和__proto__都叫原型,如果非要區分,prototype我們稱為顯示原型,__proto__我們稱為隱式原型。
person1.__proto__ == Person.prototype // true
person1.__proto__.constructor===Person.prototype.constructor //true
6.__proto__和prototype的關系
- 只有對象才具有屬性__proto__,可稱為隱式原型,一個對象的隱式原型指向構造該對象的構造函數的原型,這也保證了實例能夠訪問在構造函數原型中定義的屬性和方法。
- 方法(函數)是個特殊的對象,除了和其他對象一樣有上述__proto__屬性之外,還有自己特有的屬性——原型屬性(prototype),這個屬性指向一個對象,這個對象的用途就是包含所有實例共享的屬性和方法(我們把這個對象叫做原型對象)。原型對象也有一個屬性,叫做constructor,這個屬性指回原構造函數。
舉個例子:
var Foo=function(){}
var f1=new Foo()
var f2=new Foo()
console.log(f1.__proto__ === Foo.prototype); //true
console.log(f2.__proto__ === Foo.prototype); //true
console.log(Foo.__proto__ === Function.prototype); //true
console.log(Function.prototype.__proto__===Object.prototype); //true
console.log(Object.prototype.__proto__===null); //true
- f1和f2是Foo這個對象的兩個實例,這兩個對象也有屬性__proto__,指向構造函數的原型對象,這樣子就可以訪問Foo的原型對象的所有方法了
- 構造函數Foo()除了是方法,也是對象啊,它也有__proto__屬性,指向誰呢?指向它的構造函數的原型對象唄。函數的構造函數不就是Function嘛(
function Function(){}
),因此這里的__proto__指向了Function.prototype。 - Function.prototype是個對象,那么他有__proto__屬性,指向誰呢?指向它的構造函數的原型對象唄。對象的構造函數不就是Object嘛(
function Object()
),因此Function.prototype.__proto__===Object.prototype
- 最后,Object.prototype的__proto__屬性指向null。
總結:
- 對象有屬性__proto__,指向該對象的構造函數的原型對象。
- 方法除了有屬性__proto__,還有屬性prototype,prototype指向該方法的原型對象。
7.原型鏈
function Person(){
this.age=10
}
Person.prototype.name = 'Nicholas';
Person.prototype.age = 24;
Person.prototype.sayAge = function () {
alert(this.age);
};
//實例
var person1 = new Person();
前面的demo中我們舉了一個類似這樣的例子。
我們嘗試輸出實例的屬性。
person1.age; // 10
person1.name; // Nicholas
person1.toString; // function toString() { [native code] }
person1
我們輸出一下person1試試
從上例中,我們可以看出,Person有自己的原型,即Person.prototype,同樣Person.prototype也有自己的原型,即Person.prototype.__proto__屬性,我們輸出下試試。
console.log(Person.prototype)
console.log(Person.prototype.__proto__)
console.log(Person.prototype.__proto__.__proto__)
結果如下。
我們來總結下原型鏈。
當我們訪問實例對象的一個屬性時候,如果不存在,就去實例的原型對象里面找,這個原型對象也有自己的原型對象。就這樣一直找就構成了我們常說的線性原型鏈。
上面代碼的age來自于自身屬性,name來自于原型屬性,toString( )方法來自于Person原型對象的原型Object的原型。當我們訪問一個實例屬性的時候,如果沒有找到,我們就會繼續搜索實例的原型,如果還沒有找到,就遞歸搜索原型鏈直到原型鏈末端。
我們來驗證下
Person.prototype.__proto__ == Object.prototype // true
繼續深入驗證
Person.__proto__ == Function.prototype // true
Function.prototype.__proto__ == Object.prototype // true
我們會發現Person是Function對象的實例,Function是Object對象的實例,Person原型是Object對象的實例。
最后,奉上一張原型大圖
- 構造函數和對象原型一一對應,他們與實例一起作為三要素構成了三面這幅圖。最左側是實例,中間是構造函數,最右側是對象原型。
- 最最右側的null告訴我們:Object.prototype.__proto__ = null,也就是Object.prototype是JS中一切對象的根源。其余的對象繼承于它,并擁有自己的方法和屬性。
最后的最后,說了這么多,實際怎么用?大家下去可以看一下,js面向對象的三大特性:
封裝、繼承、多態
原型鏈主要用于繼承特性。
參考來源: