一文讓你理解什麼是 JS 原型

007kscFEgy1fxq2ayt269j31uo11iq52.jpg

前言

最近整理了一部分的 JavaScript知識點,因爲 js高級階段涉及知識點比較複雜,文章一直沒更新,這裏單獨將原型部分的概念拎出來理解下。

1.原型

1.1 傳統構造函數存在問題

經過自定義構造函數的方式,建立小狗對象:數組

function Dog(name, age) {
    this.name = name;
    this.age = age;
    this.say = function() {
        console.log('汪汪汪');
    }
}
var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黃狗', 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //輸出結果爲false

畫個圖理解下:瀏覽器

image

每次建立一個對象的時候,都會開闢一個新的空間,咱們從上圖能夠看出,每隻建立的小狗有一個say方法,這個方法都是獨立的,可是功能徹底相同。隨着建立小狗的數量增多,形成內存的浪費就更多,這就是咱們須要解決的問題。框架

爲了不內存的浪費,咱們想要的實際上是下圖的效果:函數

image

解決方法:this

這裏最好的辦法就是將函數體放在構造函數以外,在構造函數中只須要引用該函數便可。
function sayFn() {
    console.log('汪汪汪');
}

function Dog(name, age) {
    this.name = name;
    this.age = age;
    this.say = sayFn();
}
var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黃狗', 0.5);

console.log(dog1);
console.log(dog2);

console.log(dog1.say == dog2.say); //輸出結果爲true

這樣寫依然存在問題:spa

  • 全局變量增多,會增長引入框架命名衝突的風險
  • 代碼結構混亂,會變得難以維護

想要解決上面的問題就須要用到構造函數的原型概念。firefox

1.2 原型的概念

prototype:原型。每一個構造函數在建立出來的時候系統會自動給這個構造函數建立而且關聯一個空的對象。這個空的對象,就叫作原型。

關鍵點:prototype

  • 每個由構造函數建立出來的對象,都會默認的和構造函數的原型關聯;
  • 當使用一個方法進行屬性或者方法訪問的時候,會先在當前對象內查找該屬性和方法,若是當前對象內未找到,就會去跟它關聯的原型對象內進行查找;
  • 也就是說,在原型中定義的方法跟屬性,會被這個構造函數建立出來的對象所共享;
  • 訪問原型的方式:構造函數名.prototype

示例圖:調試

image

示例代碼: 給構造函數的原型添加方法code

function Dog(name,age){
    this.name = name;
    this.age = age;
}

// 給構造函數的原型 添加say方法
Dog.prototype.say = function(){
    console.log('汪汪汪');
}

var dog1 = new Dog('哈士奇', 1.5);
var dog2 = new Dog('大黃狗', 0.5);

dog1.say();  // 汪汪汪
dog2.say();  // 汪汪汪

咱們能夠看到,自己Dog這個構造函數中是沒有say這個方法的,咱們經過Dog.prototype.say的方式,在構造函數Dog的原型中建立了一個方法,實例化出來的dog1dog2會先在本身的對象先找say方法,找不到的時候,會去他們的原型對象中查找。

如圖所示:

建立一個原型對象

在構造函數的原型中能夠存放全部對象共享的數據,這樣能夠避免屢次建立對象浪費內存空間的問題。

1.3 原型的使用

一、使用對象的動態特性

使用對象的動態屬性,其實就是直接使用 prototype爲原型添加屬性或者方法。
function Person () {}

Person.prototype.say = function () {
    console.log( '講了一句話' );
};

Person.prototype.age = 18;

var p = new Person();
p.say();  // 講了一句話
console.log(p.age);  // 18

二、直接替換原型對象

每次構造函數建立出來的時候,都會關聯一個空對象,咱們能夠用一個對象替換掉這個空對象。
function Person () {}

Person.prototype = {
    say : function () {
        console.log( '講了一句話' );
    },
};

var p = new Person();
p.say();  // 講了一句話

注意:

使用原型的時候,有幾個注意點須要注意一下,咱們經過幾個案例來了解一下。
  • 使用對象.屬性名去獲取對象屬性的時候,會先在自身中進行查找,若是沒有,就去原型中查找;
// 建立一個英雄的構造函數 它有本身的 name 和 age 屬性
function Hero(){
    this.name="德瑪西亞之力";
    this.age=18;
}
// 給這個構造函數的原型對象添加方法和屬性
Hero.prototype.age= 30;
Hero.prototype.say=function(){
    console.log('人在塔在!!!');
}

var h1 = new Hero();
h1.say();   // 先去自身中找 say 方法,沒有再去原型中查找  打印:'人在塔在!!!'
console.log(p1.name);  // "德瑪西亞之力"
console.log(p1.age);   // 18 先去自身中找 age 屬性,有的話就不去原型中找了
  • 使用對象.屬性名去設置對象屬性的時候,只會在自身進行查找,若是有,就修改,若是沒有,就添加;
// 建立一個英雄的構造函數
function Hero(){
    this.name="德瑪西亞之力";
}
// 給這個構造函數的原型對象添加方法和屬性
Hero.prototype.age = 18;

var h1 = new Hero();
console.log(h1);       // {name:"德瑪西亞之力"}
console.log(h1.age);   // 18

h1.age = 30;           // 設置的時候只會在自身中操做,若是有,就修改,若是沒有,就添加 不會去原型中操做
console.log(h1);       // {name:"德瑪西亞之力",age:30}
console.log(h1.age);   // 30
  • 通常狀況下,不會將屬性放在原型中,只會將方法放在原型中;
  • 在替換原型的時候,替換以前建立的對象,和替換以後建立的對象的原型不一致!!!
// 建立一個英雄的構造函數 它有本身的 name 屬性
function Hero(){
    this.name="德瑪西亞之力";
}
// 給這個構造函數的默認原型對象添加 say 方法
Hero.prototype.say = function(){
    console.log('人在塔在!!!');
}

var h1 = new Hero();
console.log(h1);    // {name:"德瑪西亞之力"}
h1.say();           // '人在塔在!!!'

// 開闢一個命名空間 obj,裏面有個 kill 方法
var obj = {
    kill : function(){
        console.log('大寶劍');
    }
}

// 將建立的 obj 對象替換本來的原型對象
Hero.prototype = obj;

var h2 = new Hero();

h1.say();           // '人在塔在!!!'
h2.say();           // 報錯

h1.kill();          // 報錯
h2.kill();          // '大寶劍'

畫個圖理解下:

image

圖中能夠看出,實例出來的h1對象指向的原型中,只有say()方法,並無kill()方法,因此h1.kill()會報錯。同理,h2.say()也會報錯。

1.4 __proto__屬性

js中以 _開頭的屬性名爲 js的私有屬性,以 __開頭的屬性名爲非標準屬性。 __proto__是一個非標準屬性,最先由 firefox提出來。

一、構造函數的 prototype 屬性

以前咱們訪問構造函數原型對象的時候,使用的是 prototype屬性:
function Person(){}

//經過構造函數的原型屬性prototype能夠直接訪問原型
Person.prototype;
在以前咱們是沒法經過構造函數 new出來的對象訪問原型的:
function Person(){}

var p = new Person();

//之前不能直接經過p來訪問原型對象

二、實例對象的 __proto__ 屬性

__proto__屬性最先是火狐瀏覽器引入的,用以經過實例對象來訪問原型,這個屬性在早期是非標準的屬性,有了 __proto__屬性,就能夠經過構造函數建立出來的對象直接訪問原型。
function Person(){}

var p = new Person();

//實例對象的__proto__屬性能夠方便的訪問到原型對象
p.__proto__;

//既然使用構造函數的`prototype`和實例對象的`__proto__`屬性均可以訪問原型對象
//就有以下結論
p.__proto__ === Person.prototype;

如圖所示:

image

三、__proto__屬性的用途

  • 能夠用來訪問原型;
  • 在實際開發中除非有特殊的需求,不要輕易的使用實例對象的__proto__屬性去修改原型的屬性或方法;
  • 在調試過程當中,能夠輕易的查看原型的成員;
  • 因爲兼容性問題,不推薦使用。

3.5 constuctor屬性

constructor:構造函數,原型的 constructor屬性指向的是和原型關聯的構造函數。

示例代碼:

function Dog(){
    this.name="husky";
}

var d=new Dog();

// 獲取構造函數
console.log(Dog.prototype.constructor);  // 打印構造函數 Dog
console.log(d.__proto__.constructor);    // 打印構造函數 Dog

如圖所示:

image

獲取複雜類型的數據類型:

經過 obj.constructor.name的方式,獲取當前對象 obj的數據類型。

在一個的函數中,有個返回值name,它表示的是當前函數的函數名;

function Teacher(name,age){
    this.name = name;
    this.age = age;
}

var teacher = new Teacher();

// 假使咱們只知道一個對象teacher,如何獲取它的類型呢?
console.log(teacher.__proto__.constructor.name);  // Teacher

console.log(teacher.constructor.name);  // Teacher

實例化出來的teacher對象,它的數據類型是啥呢?咱們能夠經過實例對象teacher.__proto__,訪問到它的原型對象,再經過.constructor訪問它的構造函數,經過.name獲取當前函數的函數名,因此就能獲得當前對象的數據類型。又由於.__proto__是一個非標準的屬性,並且實例出的對象繼承原型對象的方法,因此直接能夠寫成:obj.constructor.name

1.6 原型繼承

原型繼承:每個構造函數都有 prototype原型屬性,經過構造函數建立出來的對象都繼承自該原型屬性。因此能夠經過更改構造函數的原型屬性來實現繼承。

繼承的方式有多種,能夠一個對象繼承另外一個對象,也能夠經過原型繼承的方式進行繼承。

一、簡單混入繼承

直接遍歷一個對象,將全部的屬性和方法加到另外一對象上。
var animal = {
    name:"Animal",
    sex:"male",
    age:5,
    bark:function(){
        console.log("Animal bark");
    }
};

var dog = {};

for (var k in animal){
    dog[k]= animal[k];
}

console.log(dog);  // 打印的對象與animal如出一轍

缺點:只能一個對象繼承自另外一個對象,代碼複用過低了。

二、混入式原型繼承

混入式原型繼承其實與上面的方法相似,只不過是將遍歷的對象添加到構造函數的原型上。
var obj={
     name:'zs',
     age:19,
     sex:'male'
 }

function Person(){
    this.weight=50;
}

for(var k in obj){
    // 將obj裏面的全部屬性添加到 構造函數 Person 的原型中
    Person.prototype[k] = obj[k];
}

var p1=new Person();
var p2=new Person();
var p3=new Person();

console.log(p1.name);  // 'zs'
console.log(p2.age);   // 19
console.log(p3.sex);   // 'male'

面向對象思想封裝一個原型繼承

咱們能夠利用面向對象的思想,將面向過程進行封裝。
function Dog(){
    this.type = 'yellow Dog';
}

// 給構造函數 Dog 添加一個方法 extend
Dog.prototype.extend = function(obj){
    // 使用混入式原型繼承,給 Dog 構造函數的原型繼承 obj 的屬性和方法
     for (var k in obj){
        this[k]=obj[k];
    }
}

// 調用 extend 方法
Dog.prototype.extend({
    name:"二哈",
    age:"1.5",
    sex:"公",
    bark:function(){
        console.log('汪汪汪');
    }
});

三、替換式原型繼承

替換式原型繼承,在上面已經舉過例子了,其實就是將一個構造函數的原型對象替換成另外一個對象。
function Person(){
    this.weight=50;
}

var obj={
    name:'zs',
    age:19,
    sex:'male'
}
// 將一個構造函數的原型對象替換成另外一個對象
Person.prototype = obj;

var p1=new Person();
var p2=new Person();
var p3=new Person();

console.log(p1.name);  // 'zs'
console.log(p2.age);   // 19
console.log(p3.sex);   // 'male'

以前咱們就說過,這樣作會產生一個問題,就是替換的對象會從新開闢一個新的空間。

替換式原型繼承時的bug

替換原型對象的方式會致使原型的 constructor的丟失, constructor屬性是默認原型對象指向構造函數的,就算是替換了默認原型對象,這個屬性依舊是默認原型對象指向構造函數的,因此新的原型對象是沒有這個屬性的。

image

解決方法:手動關聯一個constructor屬性

function Person() {
    this.weight = 50;
}

var obj = {
    name: 'zs',
    age: 19,
    sex: 'male'
}
// 在替換原型對象函數以前 給須要替換的對象添加一個 constructor 屬性 指向本來的構造函數
obj.constructor = Person;

// 將一個構造函數的原型對象替換成另外一個對象
Person.prototype = obj;

var p1 = new Person();

console.log(p1.__proto__.constructor === Person);  // true

四、Object.create()方法實現原型繼承

當咱們想把 對象1做爲 對象2的原型的時候,就能夠實現 對象2繼承 對象1。前面咱們瞭解了一個屬性: __proto__,實例出來的對象能夠經過這個屬性訪問到它的原型,可是這個屬性只適合開發調試時使用,並不能直接去替換原型對象。因此這裏介紹一個新的方法: Object.create()

語法: var obj1 = Object.create(原型對象);

示例代碼: 讓空對象obj1繼承對象obj的屬性和方法

var obj = {
    name : '蓋倫',
    age : 25,
    skill : function(){
        console.log('大寶劍');
    }
}

// 這個方法會幫咱們建立一個原型是 obj 的對象
var obj1 = Object.create(obj);

console.log(obj1.name);     // "蓋倫"
obj1.skill();               // "大寶劍"

兼容性:

因爲這個屬性是 ECMAScript5的時候提出來的,因此存在兼容性問題。

利用瀏覽器的能力檢測,若是存在Object.create則使用,若是不存在的話,就建立構造函數來實現原型繼承。

// 封裝一個能力檢測函數
function create(obj){
    // 判斷,若是瀏覽器有 Object.create 方法的時候
    if(Object.create){
        return Object.create(obj);
    }else{
        // 建立構造函數 Fun
        function Fun(){};
        Fun.prototype = obj; 
        return new Fun();
    }
}

var hero = {
    name: '蓋倫',
    age: 25,
    skill: function () {
        console.log('大寶劍');
    }
}

var hero1 = create(hero);
console.log(hero1.name);    // "蓋倫"
console.log(hero1.__proto__ == hero);   // true

2.原型鏈

對象有原型,原型自己又是一個對象,因此原型也有原型,這樣就會造成一個鏈式結構的原型鏈。

2.1 什麼是原型鏈

示例代碼: 原型繼承練習

// 建立一個 Animal 構造函數
function Animal() {
    this.weight = 50;
    this.eat = function() {
        console.log('蜂蜜蜂蜜');
    }
}

// 實例化一個 animal 對象
var animal = new Animal();

// 建立一個 Preson 構造函數
function Person() {
    this.name = 'zs';
    this.tool = function() {
        console.log('菜刀');
    }
}

// 讓 Person 繼承 animal (替換原型對象)
Person.prototype = animal;

// 實例化一個 p 對象 
var p = new Person();

// 建立一個 Student 構造函數
function Student() {
    this.score = 100;
    this.clickCode = function() {
        console.log('啪啪啪');
    }
}

// 讓 Student 繼承 p (替換原型對象)
Student.prototype = p;

//實例化一個 student 對象
var student = new Student();


console.log(student);           // 打印 {score:100,clickCode:fn}

// 由於是一級級繼承下來的 因此最上層的 Animate 裏的屬性也是被繼承的
console.log(student.weight);    // 50
student.eat();         // 蜂蜜蜂蜜
student.tool();        // 菜刀

如圖所示:

咱們將上面的案例經過畫圖的方式展示出來後就一目瞭然了,實例對象 animal直接替換了構造函數 Person的原型,以此類推,這樣就會造成一個鏈式結構的原型鏈。

image

完整的原型鏈

結合上圖,咱們發現,最初的構造函數 Animal建立的同時,會建立出一個原型,此時的原型是一個空的對象。結合原型鏈的概念:「原型自己又是一個對象,因此原型也有原型」,那麼這個空對象往上還能找出它的原型或者構造函數嗎?

咱們如何建立一個空對象? 一、字面量:{};二、構造函數:new Object()。咱們能夠簡單的理解爲,這個空的對象就是,構造函數Object的實例對象。因此,這個空對象往上面找是能找到它的原型和構造函數的。

// 建立一個 Animal 構造函數
function Animal() {
    this.weight = 50;
    this.eat = function() {
        console.log('蜂蜜蜂蜜');
    }
}

// 實例化一個 animal 對象
var animal = new Animal();

console.log(animal.__proto__);      // {}
console.log(animal.__proto__.__proto__);  // {}
console.log(animal.__proto__.__proto__.constructor);  // function Object(){}
console.log(animal.__proto__.__proto__.__proto__);  // null

如圖所示:

image

2.2 原型鏈的拓展

一、描述出數組[]的原型鏈結構

// 建立一個數組
var arr = new Array();

// 咱們能夠看到這個數組是構造函數 Array 的實例對象,因此他的原型應該是:
console.log(Array.prototype);   // 打印出來仍是一個空數組

// 咱們能夠繼續往上找 
console.log(Array.prototype.__proto__);  // 空對象

// 繼續
console.log(Array.prototype.__proto__.__proto__)  // null

如圖所示:

image

二、擴展內置對象

js原有的內置對象,添加新的功能。

注意:這裏不能直接給內置對象的原型添加方法,由於在開發的時候,你們都會使用到這些內置對象,假如你們都是給內置對象的原型添加方法,就會出現問題。

錯誤的作法:

// 第一個開發人員給 Array 原型添加了一個 say 方法
Array.prototype.say = function(){
    console.log('哈哈哈');
}

// 第二個開發人員也給 Array 原型添加了一個 say 方法
Array.prototype.say = function(){
    console.log('啪啪啪');
}

var arr = new Array();

arr.say();  // 打印 「啪啪啪」  前面寫的會被覆蓋

爲了不出現這樣的問題,只需本身定義一個構造函數,而且讓這個構造函數繼承數組的方法便可,再去添加新的方法。

// 建立一個數組對象 這個數組對象繼承了全部數組中的方法
var arr = new Array();

// 建立一個屬於本身的構造函數
function MyArray(){}

// 只須要將本身建立的構造函數的原型替換成 數組對象,就能繼承數組的全部方法
MyArray.prototype = arr;

// 如今能夠單獨的給本身建立的構造函數的原型添加本身的方法
MyArray.prototype.say = function(){
    console.log('這是我本身添加的say方法');
}

var arr1 = new MyArray();

arr1.push(1);   // 建立的 arr1 對象可使用數組的方法
arr1.say();     // 也可使用本身添加的方法  打印「這是我本身添加的say方法」
console.log(arr1);  // [1]

2.3 屬性的搜索原則

當經過 對象名.屬性名獲取屬性是 ,會遵循如下屬性搜索的原則:
  • 1-首先去對象自身屬性中找,若是找到直接使用,
  • 2-若是沒找到去本身的原型中找,若是找到直接使用,
  • 3-若是沒找到,去原型的原型中繼續找,找到直接使用,
  • 4-若是沒有會沿着原型不斷向上查找,直到找到null爲止。
相關文章
相關標籤/搜索