在典型的 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()
建構函式是一種特殊的函數,主要用來初始化物件,即為物件成員變數賦初始值,它總與new一起使用。我們可以把物件中一些公共的屬性和方法抽取出來,然後封裝到這個函數裡面。
在JS中使用建構函式是要注意以下兩點:
建構函式用於建立某一類物件,其首字母要大寫
建構函式要和new一起使用才有意義
new在執行時會做四件事情:
在記憶體中建立一個新的空物件
讓this指向這個新的物件
執行建構函式裡面的程式碼,給這個新物件新增屬性和方法
返回這個新物件(所有建構函式裡面不需要renturn)
範例成員:
建構函式內部通過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')
建構函式方法很好用,但存在浪費記憶體的問題:
如: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為原型物件
原型的作用是什麼? ------共用方法
物件都會有一個屬性 __ 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。
物件原型(__ 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("電影")
}
}
總結:建構函式和原型物件之間有互相可以表明對方身份的」信物「:prototype 和 constructor。
ldh物件範例的原型(__ proto __)可以找到它的物件原型——Star原型物件prototype;
通過Star原型物件prototype的原型(__ proto __),可以找到它的物件原型——Object原型物件 prototype;
通過Object原型物件 prototype的原型(__ proto __),可以找到它的物件原型——null(最頂層)。
因此形成的線路叫做原型鏈,為我們提供了某個屬性或者函數的查詢的線路。
當存取一個物件的屬性(包括方法)時,首先查詢這個物件自身有沒有該屬性
如果沒有就查詢它的原型(也就是__ proto __ 指向的prototype原型物件)
如果還沒有就查詢原型物件的原型(Object的原型物件)
以此類推一直找到最頂層(null)
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指向誰。
擴充套件內建物件
可以通過原型物件對原來的內建物件進行擴充套件自定義的方法。比如:給陣列增加自定義求和的功能
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中看到擴充套件的求和方法
ES6 之前並沒有給我們提供extends繼承。我們可以通過建構函式+原型物件模擬實現繼承,被稱為組合繼承。
呼叫這個函數,並且修改函數執行時的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
核心原理:通過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
此處有一個思考:
在下圖中,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範例可以歸溯自己的建構函式是誰.