1.構造函數和原型 1.1 概述 在典型的 OOP語言中(如Java),都存在類的概念,類就是對象的模板,對象就是類的實例,但在ES6之前,JS並沒有引入類的概念。 在ES6之前,對象不是基於類創建的,而是一種稱為構建函數的特殊函數來定義對象和它們的特征。 有三種創建對象的方式: 對象字面量(con ...
1.構造函數和原型
1.1 概述
在典型的 OOP語言中(如Java),都存在類的概念,類就是對象的模板,對象就是類的實例,但在ES6之前,JS並沒有引入類的概念。
在ES6之前,對象不是基於類創建的,而是一種稱為構建函數的特殊函數來定義對象和它們的特征。
有三種創建對象的方式:
-
對象字面量(const obj = {name:'ab'})
-
new Object()
-
自定義構造函數
//構造函數
function Star(uname,age){
this.uname = uname;
this.age = age
this.sing = function(){
console.log("我會唱歌")
}
}
const ldh = new Star("劉德華",18)
const syz = new Star("孫燕姿",17)
console.log(ldh)
ldh.sing()
syz.sing()
1.2 構造函數
構造函數是一種特殊的函數,主要用來初始化對象,即為對象成員變數賦初始值,它總與new一起使用。我們可以把對象中一些公共的屬性和方法抽取出來,然後封裝到這個函數裡面。
在JS中使用構造函數是要註意以下兩點:
-
構造函數用於創建某一類對象,其首字母要大寫
-
構造函數要和new一起使用才有意義
new在執行時會做四件事情:
-
在記憶體中創建一個新的空對象
-
讓this指向這個新的對象
-
執行構造函數裡面的代碼,給這個新對象添加屬性和方法
-
返回這個新對象(所有構造函數裡面不需要renturn)
1.3 實例成員和靜態成員
實例成員:
構造函數內部通過this添加的成員 ,如下圖的uname,age,sing就是實例成員
實例成員只能通過實例化的對象來訪問
function Star(uname,age){
this.uname = unamw;
this.age = age
this.sing = function(){
console.log("我會唱歌")
}
}
const ldh = new Star("劉德華",18)
靜態成員:
靜態成員在構造函數本身上添加的成員,如下圖的sex就是靜態成員
鏡頭成員只能通過構造函數來訪問不能通過對象訪問。
Star.sex = '男'
console.log('Star.sex')
1.4構造函數的問題
構造函數方法很好用,但存在浪費記憶體的問題:
如:Star()構造函數中的sing()方法在每次實例化對象的時候都需要單獨開闢一份記憶體空間,存在浪費空間的問題。
但是我們希望所有的對象使用同一個函數,這樣就比較節省記憶體。
//構造函數
function Star(uname, age) {
this.uname = uname;
this.age = age;
this.sing = function () {
console.log("我會唱歌");
};
}
const ldh = new Star("劉德華", 18);
const syz = new Star("孫燕姿", 17);
思考:可是,為什麼每次實例化都是單獨開闢空間呢?
這個問題我查閱了很多的資料,總結一句話就是:在JS中,引用類型被創建的時候都會開闢一個新的空間。(其中的知識點比較多,詳情請期待下一篇文章~)
構造函數通過原型分配的函數是所有對象所共用的。
JavaScript規定,每一個構造函數都有一個prototype 屬性,指向另一個對象。
註意: 這個prototype就是一個對象,這個對象所有的屬性和方法都會被構造函數所擁有。
我們可以把那些不變的方法,直接定義在prototype 對象上,這樣所有對象的實例就可以共用這些方法。
function Star(uname,age){
this.uname = unamw;
this.age = age
}
Star.sing = function(){
console.log("我會唱歌")
}
const syz = new Star('孫燕姿',20)
syz.sing()//我會唱歌
-
原型是什麼? -------是一個對象,我們也稱prototype為原型對象
-
原型的作用是什麼? ------共用方法
1.6對象原型 __ proto __
對象都會有一個屬性 __ prpto __ 指向構造函數的 prototype 原型對象,之所以對象可以使用構造函數 prototype 原型對象的屬性和方法,就是因為對象有__ proto __ 原型的存在。
總結:
-
__ proto __ 對象原型和原型對象 prototype 是等價的
function Star(uname,age){
this.uname = unamw;
this.age = age
}
Star.sing = function(){
console.log("我會唱歌")
}
const syz = new Star('孫燕姿',20)
console.log(syz.__ptoto === Star.prototype)//等價
-
__ proto __ 對象原型的意義就在於為對象的查找機制提供一個方向或者一條線路,但是它是一個非標準屬性,因此實際開發中,不可以使用這個屬性,它只是內部指向原型對象prototype。
1.7 constructor構造函數
對象原型(__ proto __)和構造函數原型對象(prototype)裡面都有一個屬性:constructor屬性,constructor 我們稱為構造函數,因為它指回構造函數本身。
function Star(uname,age){
this.uname = unamw;
this.age = age
}
Star.prototype = {
//這種情況下我們已經修改了原來prototype,給原型對象賦值的是一個對象,所有必須手動的把 constructor指回原來的構造函數
constructor:Star,
sing:function(){
console.log("唱歌")
}
movie:function(){
console.log("電影")
}
}
1.8構造函數實例原型對象三者之間的關係
總結:構造函數和原型對象之間有互相可以表明對方身份的”信物“:prototype 和 constructor。
1.9 原型鏈
總結:
ldh對象實例的原型(__ proto __)可以找到它的對象原型——Star原型對象prototype;
通過Star原型對象prototype的原型(__ proto __),可以找到它的對象原型——Object原型對象 prototype;
通過Object原型對象 prototype的原型(__ proto __),可以找到它的對象原型——null(最頂層)。
因此形成的線路叫做原型鏈,為我們提供了某個屬性或者函數的查找的線路。
1.10 原型鏈查找規則
-
當訪問一個對象的屬性(包括方法)時,首先查找這個對象自身有沒有該屬性
-
如果沒有就查找它的原型(也就是__ proto __ 指向的prototype原型對象)
-
如果還沒有就查找原型對象的原型(Object的原型對象)
-
以此類推一直找到最頂層(null)
1.11原型對象的this指向
function Star(uname,age){
this.uname = unamw;
this.age = age
}
let that;
Star.prototype.sing = function(){
that = this
console.log("我會唱歌")
}
const ldh = new Star("劉德華",18)
ldh.sing()
console.log(that === ldh)//true
在構造函數(star)中 this 指向 創建的實例對象(ldh)。
sing函數只有調用之後才能確然this的指向,一般原則是:誰調用,this指向誰。
1.12 原型對象的應用
擴展內置對象
可以通過原型對象對原來的內置對象進行擴展自定義的方法。比如:給數組增加自定義求和的功能
Array.prototype.sum = function(){
let sum = 0
for(let i = 0;i < this.length; i++){
sum += this[i]
}
return sum;
}
let arr1 = [1,4,5,6,8,9]
console.log(arr1.sum())//33
cosole.log(Array.prototype)//可以在arry中看到擴展的求和方法
2.繼承
ES6 之前並沒有給我們提供extends繼承。我們可以通過構造函數+原型對象模擬實現繼承,被稱為組合繼承。
2.1call()
調用這個函數,並且修改函數運行時的this指向。
fun.call(thisArg, arg1, arg2... )
-
thisArg:當前調用函數this的指向對象
-
arg1 ,arg2: 傳遞的其他對象
const o = {
name: 'zooey'
}
function fn(x,y){
console.log("輸出這個函數")
console.log(this) // 此時 this 是 window
console.log(x+y)
}
//普通調用的方式
fn()
//call調用方式: 使用對象調用, 此時的 this 是 o
fn.call(o) //此時 this 輸出是 O
//call 做一些其他操作 比如:求出後兩個參數的和
fn.call(o,5,2) //此時 this 輸出是 O
2.2借用構造函數繼承父類屬性
核心原理:通過call 把父類的this 指向子類型的this ,這樣就可以實現子類型繼承夫類型的屬性。
function Father(uname, age) {
this.uname = uname;
this.age = age;
}
function Son(uname, age, score) {
/**
使用call調用Father 構造方法,
並且把Father構造方法的this ,修改為Son的調用者
傳遞參數uname,age,將參數也綁定給Father
*/
Father.call(this, uname, age);
this.score = score;
}
const son = new Son("lan", 26, 100);
console.log(son);
註意點:
在 Son 構造函數中第一個參數是將 Son 的this傳遞給Father ,但是Son只有在實例化時才能知道this是誰。
初始化的參數通過Son傳遞給Father
2.3借用原型對象繼承父類型方法
此處有一個思考:
在下圖中,Array的原型對象中增加求和方法sum,在實例對arr1中就可以直接使用,但是在非實例化的形式中,如何實現方法的繼承呢?
案例
function Father(uname, age) {
this.uname = uname;
this.age = age;
}
Father.prototype.getMoney = function () {
console.log("掙錢");
};
function Son(uname, age, score) {
/**
使用call調用Father 構造方法,
並且把Father構造方法的this ,修改為Son的調用者
傳遞參數uname,age,將參數也綁定給Father
*/
Father.call(this, uname, age);
this.score = score;
}
Son.prototype.exam = function () {
console.log("孩子考試");
};
const son = new Son("lan", 26, 100);
console.log(son);
son.exam();
son.getMoney();
| 可以看到上圖中,son實例對象不能調用getMoney()方法
| 也不能將Father的prototype直接賦值給Son ,如下圖
//直接賦值的操作
Son.prototype = Father.prototype;
Son.prototype.exam = function () {
console.log("孩子考試");
};
//此時對Son的prototype修改也會是 Father擁有 exam方法,因為現在是: Son的prototype 指向了Father 的 prototype,兩個構造函數本質上是一個prototype.
console.log(Father);
註意: 此時對Son的prototype修改也會是 Father擁有 exam方法,因為現在是: Son的prototype 指向了Father 的 prototype,兩個構造函數本質上是一個prototype.
正確的做法:
Son.prototype = new Father();
//記得將constructor指回 Son構造函數
Son.prototype.constructor = Son;
Son.prototype.exam = function () {
console.log("孩子考試");
};
console.log(Father);
思考:記得將constructor指回 Son構造函數 的原因是什麼呢?
嘗試註釋掉指回構造函數的代碼,也可以正常輸出,只不過沒有constructor參數
只是son實例可以歸溯自己的構造函數是誰.