最近整理了一部分的JavaScript
知識點,因爲js
高級階段涉及知識點比較複雜,文章一直沒更新,這裏單獨將原型部分的概念拎出來理解下。
經過自定義構造函數的方式,建立小狗對象:數組
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
畫個圖理解下:瀏覽器
每次建立一個對象的時候,都會開闢一個新的空間,咱們從上圖能夠看出,每隻建立的小狗有一個say
方法,這個方法都是獨立的,可是功能徹底相同。隨着建立小狗的數量增多,形成內存的浪費就更多,這就是咱們須要解決的問題。框架
爲了不內存的浪費,咱們想要的實際上是下圖的效果:函數
解決方法: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
prototype
:原型。每一個構造函數在建立出來的時候系統會自動給這個構造函數建立而且關聯一個空的對象。這個空的對象,就叫作原型。
關鍵點:prototype
構造函數名.prototype
。示例圖:調試
示例代碼: 給構造函數的原型添加方法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
的原型中建立了一個方法,實例化出來的dog1
、dog2
會先在本身的對象先找say
方法,找不到的時候,會去他們的原型對象中查找。
如圖所示:
在構造函數的原型中能夠存放全部對象共享的數據,這樣能夠避免屢次建立對象浪費內存空間的問題。
一、使用對象的動態特性
使用對象的動態屬性,其實就是直接使用
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(); // '大寶劍'
畫個圖理解下:
圖中能夠看出,實例出來的h1
對象指向的原型中,只有say()
方法,並無kill()
方法,因此h1.kill()
會報錯。同理,h2.say()
也會報錯。
在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;
如圖所示:
三、__proto__屬性的用途
__proto__
屬性去修改原型的屬性或方法;constructor
:構造函數,原型的constructor
屬性指向的是和原型關聯的構造函數。
示例代碼:
function Dog(){ this.name="husky"; } var d=new Dog(); // 獲取構造函數 console.log(Dog.prototype.constructor); // 打印構造函數 Dog console.log(d.__proto__.constructor); // 打印構造函數 Dog
如圖所示:
獲取複雜類型的數據類型:
經過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
。
原型繼承
:每個構造函數都有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
屬性是默認原型對象指向構造函數的,就算是替換了默認原型對象,這個屬性依舊是默認原型對象指向構造函數的,因此新的原型對象是沒有這個屬性的。
解決方法:手動關聯一個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
對象有原型,原型自己又是一個對象,因此原型也有原型,這樣就會造成一個鏈式結構的原型鏈。
示例代碼: 原型繼承練習
// 建立一個 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
的原型,以此類推,這樣就會造成一個鏈式結構的原型鏈。
完整的原型鏈
結合上圖,咱們發現,最初的構造函數
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
如圖所示:
一、描述出數組[]的原型鏈結構
// 建立一個數組 var arr = new Array(); // 咱們能夠看到這個數組是構造函數 Array 的實例對象,因此他的原型應該是: console.log(Array.prototype); // 打印出來仍是一個空數組 // 咱們能夠繼續往上找 console.log(Array.prototype.__proto__); // 空對象 // 繼續 console.log(Array.prototype.__proto__.__proto__) // null
如圖所示:
二、擴展內置對象
給
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]
當經過
對象名.屬性名
獲取屬性是 ,會遵循如下屬性搜索的原則:
null
爲止。