js原型、原型鏈

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
1.png

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__我們稱為隱式原型。

22.png
person1.__proto__ == Person.prototype // true
person1.__proto__.constructor===Person.prototype.constructor  //true

6.__proto__和prototype的關系

  1. 只有對象才具有屬性__proto__,可稱為隱式原型,一個對象的隱式原型指向構造該對象的構造函數的原型,這也保證了實例能夠訪問在構造函數原型中定義的屬性和方法。
  2. 方法(函數)是個特殊的對象,除了和其他對象一樣有上述__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
  1. f1和f2是Foo這個對象的兩個實例,這兩個對象也有屬性__proto__,指向構造函數的原型對象,這樣子就可以訪問Foo的原型對象的所有方法了
  2. 構造函數Foo()除了是方法,也是對象啊,它也有__proto__屬性,指向誰呢?指向它的構造函數的原型對象唄。函數的構造函數不就是Function嘛(function Function(){}),因此這里的__proto__指向了Function.prototype。
  3. Function.prototype是個對象,那么他有__proto__屬性,指向誰呢?指向它的構造函數的原型對象唄。對象的構造函數不就是Object嘛(function Object()),因此Function.prototype.__proto__===Object.prototype
  4. 最后,Object.prototype的__proto__屬性指向null。

總結:

  1. 對象有屬性__proto__,指向該對象的構造函數的原型對象。
  2. 方法除了有屬性__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試試

im.jpg

從上例中,我們可以看出,Person有自己的原型,即Person.prototype,同樣Person.prototype也有自己的原型,即Person.prototype.__proto__屬性,我們輸出下試試。

console.log(Person.prototype)
console.log(Person.prototype.__proto__)
console.log(Person.prototype.__proto__.__proto__)

結果如下。

img1.jpg

我們來總結下原型鏈。

當我們訪問實例對象的一個屬性時候,如果不存在,就去實例的原型對象里面找,這個原型對象也有自己的原型對象。就這樣一直找就構成了我們常說的線性原型鏈。

上面代碼的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對象的實例。

最后,奉上一張原型大圖

33.jpg
  • 構造函數和對象原型一一對應,他們與實例一起作為三要素構成了三面這幅圖。最左側是實例,中間是構造函數,最右側是對象原型。
  • 最最右側的null告訴我們:Object.prototype.__proto__ = null,也就是Object.prototype是JS中一切對象的根源。其余的對象繼承于它,并擁有自己的方法和屬性。

最后的最后,說了這么多,實際怎么用?大家下去可以看一下,js面向對象的三大特性:
封裝、繼承、多態

原型鏈主要用于繼承特性。

參考來源:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容