1.原型和原型鏈的概念
js在建立一個物件時,比如叫 obj
,都會給他偷偷的加上一個引用,這個引用指向的是一個物件,比如叫 yuanxing
,
這個物件可以給引用它的物件提供屬性共享
,比如:yuanxing
上有個屬性name,可以被 obj.name
訪問到,
這個可以提供屬性共享的物件,就稱為前面物件的原型
而原型本身也是一個物件,所以它也會有一個自己的原型,這一層一層的延續下去,直到最後指向 null,這就形成的 原型鏈
那js的這一種是原型機制是怎麼實現的呢?
2.原型的實現
我們先從一個例子來看:
//code-01
let obj = new Object({name:'xiaomin'})
console.log(obj.name)
console.log(obj.toString())
// xiaomin
// [object Object]
我們首先建立了一個物件obj
,它有一個屬性name
屬性name
是我們建立的,但是在建立obj
的時候,並沒有給它建立toString
屬性,為什麼obj.toString()
可以訪問的到呢?
prototype 屬性
我們先來看一下Object.prototype
屬性
我們發現這裡有toString
屬性,所以其實Object.prototype
就是obj
的原型,按照原型的概念,它能夠為obj
提供屬性共享
所以當obj.toString()
的時候,先在obj
的屬性裡找,沒找到,就在obj
的原型Object.prototype
的屬性上找,可以找到,所以呼叫成功
proto 屬性
那麼obj
是怎麼找到原型的呢?我們列印obj
屬性看一下:
我們發現obj
除了我們建立的name
以外,還有一個__proto__
屬性,它的值是一個物件,那麼它等於什麼呢?
我們發現obj.__proto__
指向的就是Object.prototype
到此為止,我們可以簡單總結一下js語言實現原型的基本機制了
- 在建立一個物件
obj
時,會給它加上一個__proto__
屬性 __proto__
屬性 指向obj
的建構函式
的prototype
物件- 當訪問
obj
的某個屬性時,會先在它自己的屬性上找,如果沒找到,就在它的原型
(其實就是__proto__
指向的物件)的屬性上找
建構函式
這個有一個建構函式
的概念,其實建構函式
也就是普通函式,當這個函式被用來 new
一個新物件時,它就被稱為新物件的 建構函式
就上面的例子而言,Objec
就是obj
的建構函式,
這裡要區別一下Object
和object
的區別,前者是一個js內建的一個函式,後者是js的基本資料型別(number,string,function,object,undefined)
3.new 實際上做了什麼
上面有說到new
關鍵字,那麼在它實際上做了什麼呢?
上面程式碼code-01
使用系統內建的函式Object
來建立物件的,那麼我們現在用自己建立的函式來建立一個新物件看看:
//code-02
function human(name){
this.name = name
}
human.prototype.say = function(){
alert('我叫'+this.name)
}
let xiaomin = new human('xiaoming')
console.log(xiaomin.name)
xiaomin.say()
這裡的human
就是新物件xiaoming
的建構函式
我們把新建立的物件xiaoming
列印出來看看:
我們看到xiaoming
有一個屬性name
,並且xiaoming.__proto__
完全等於建構函式的human.prototype
,這就是它的原型
從這裡我們可以總結一下new
的基本功能:
- 把
建構函式
的this指向新建立的物件xiaoming
- 為新物件建立
原型
,其實就是把新物件的__proto__
指向建構函式的prototype
手寫new
上面我們瞭解了new
具體做了什麼事情,那麼它是怎麼實現這些功能的呢?下面我們手寫一個函式myNew
來模擬一下new
的效果:
//code-03
function human(name, age) {
this.name = name;
this.age = age;
}
human.prototype.say = function () {
console.log("my name is " + this.name);
};
xiaoming = myNew(human, "xiaoming", 27);
function myNew() {
let obj = new Object();
//取出函式的第一個引數,其實就是 human函式
let argArr = Array.from(arguments);
const constructor = argArr.shift();
// 指定原型
obj.__proto__ = constructor.prototype;
//改變函式執行環境
constructor.apply(obj, argArr);
return obj;
}
xiaoming.say();
我們先把新物件xiaoming
列印出來看一下:
我們發現這和上面的程式碼code-02
的效果是一樣的
上面程式碼code-03
裡面如果對apply
的作用不太熟悉的,可以另外瞭解一下,其實也很簡單,意思就是:在obj的環境下,執行constructor函式,argArr是函式執行時的引數,也就是指定了函式的this
4.繼承的實現
其實就上面的內容,就可以對js的原型機制有個基本的瞭解,但是一般面試的時候,如果有問到原型,接下來就會問 能不能實現 繼承
的功能,所以我們來手寫一下 原型的繼承,其實所用到的知識點都是上面有提到的
繼承的概念
我們先來說下繼承
的概念:
繼承
其實就是 一個建構函式(子類)可以使用另一個建構函式(父類)的屬性和方法,這裡有幾點注意的:
- 繼承是 建構函式 對 另一個建構函式而言
- 需要實現屬性的繼承,即
this
的轉換 - 需要實現方法的繼承,一般就是指
原型鏈
的構建
繼承的實現
基於上面的3點要素,我們先直接來看程式碼:
// code-04
// 父級 函式
function human(name) {
this.name = name;
}
human.prototype.sayName = function () {
console.log("我的名字是:", this.name);
};
// 子級 函式
function user(args) {
this.age = args.age;
//1.私有屬性的繼承
human.call(this, args.name);
//2.原型的繼承
Object.setPrototypeOf(user.prototype, human.prototype); //原型繼承-方法1
// user.prototype.__proto__ = human.prototype; // 原型繼承-方法2
}
// 因為重新賦值了prototype,所以放置 user 外部
// user.prototype = new human();//原型繼承-方法3
// user.prototype = Object.create(human.prototype);//原型繼承-方法4
user.prototype.sayAge = function () {
console.log("我的年齡是:", this.age);
};
let person = new human("人類");
let xiaoming = new user({ name: "xiaoming", age: 27 });
console.log("----父類-----");
console.log(person);
person.sayName();
console.log("----子類-----");
console.log(xiaoming);
xiaoming.sayName();
xiaoming.sayAge();
我們先來看下列印的結果:
從列印結果,我們可以看到xiaoming
擁有person
的屬性和方法(name
,sayName
),又有自己私有的屬性方法(age
,sayAge
),這是因為建構函式user
實現了對human
的繼承。
其實實現的方法無非也就是我們前面有說到的 作用域的改變和原型鏈的構造,其中作用域的改變(this指向的改變)主要是兩個方法:call和apply,原型鏈的構造原理只有一個,就是物件的原型等於其建構函式的prototype屬性
,但是實現方法有多種,程式碼code-04
中有列出4種。
從上面的例子來看原型鏈的指向是:xiaoming
->user.prototype
->human.prototype
5.class和extends
我們可能有看到一些程式碼直接用 class
和 extends
關鍵字來實現類和繼承,其實這是ES6的語法,其實是一種語法糖,本質上的原理也是相同的。我們先來看看基本用法:
用法
//code-05
class human {
//1.必須要有建構函式
constructor(name) {
this.name = name;
}//2.不能有逗號`,`
sayName() {
console.log("sayName:", this.name);
}
}
class user extends human {
constructor(params) {
//3.子類必須用`super`,呼叫父類的建構函式
super(params.name);
this.age = params.age;
}
sayAge() {
console.log("sayAge:", this.age);
}
}
let person = new human("人類");
let xiaoming = new user({ name: "xiaoming", age: 27 });
console.log("----<human> person-----");
console.log(person);
person.sayName();
console.log("----<user> xiaoming-----");
console.log(xiaoming);
xiaoming.sayName();
xiaoming.sayAge();
執行結果:
我們看到執行的結果和上面的程式碼code-04
是一樣的,但是程式碼明顯清晰了很多。幾個注意的地方:
- class類中必須要有建構函式
constructor
, - class類中的函式不能用
,
分開 - 如果要繼承父類的話,在子類的建構函式中,必須先執行
super
來呼叫的父類的建構函式
相同
上面有說class的寫法其實原理上和上面是一樣的,我們來驗證一下
-
首先看看
user
和human
是什麼型別
這裡看出來了,所以雖然被class修飾,本質上還是函式,和程式碼code-04
中的user
,human
函式是一樣的 -
再來看看prototype屬性
這裡看出來sayName
,sayAge
都是定義在human.prototype
和user.prototype
上,和程式碼code-04
中也是一樣的 -
我們再來看看原型鏈
這與程式碼code-04
中的原型鏈的指向也是一樣:xiaoming
->user.prototype
->human.prototype
差異
看完相同點,現在我們來看看不同點:
- 首先寫法上的不同
- class宣告的函式,必須要用new呼叫
- class內部的成員函式沒有prototype屬性,不可以用new呼叫
- class 內的程式碼自動是嚴格模式
- class宣告不存在變數提升,這一點和 let一樣,比如:
//code-06 console.log(name_var); var name_var = "xiaoming"; //undefined,不會報錯,var宣告存在變數提升 console.log(name_let); let name_let = "xiaoming"; // Uncaught ReferenceError: Cannot access 'name_let' before initialization //報錯,let宣告不存在變數提升 new user(); class user {} // Uncaught ReferenceError: Cannot access 'user' before initialization //報錯,class宣告不存在變數提升
- class內的方法都是不可列舉的,比如:
執行結果://code-07 class human_class { constructor(name) { this.name = name; } sayName() { console.log("sayName:", this.name); } } function human_fun(name) { this.name = name; } human_fun.prototype.sayName = function () { console.log("sayName:", this.name); }; console.log("----------human_class-----------"); console.log("prototype屬性", human_class.prototype); console.log("prototype 列舉", Object.keys(human_class.prototype)); console.log("----------human_fun-----------"); console.log("prototype屬性", human_fun.prototype); console.log("prototype 列舉", Object.keys(human_fun.prototype));
6.總結
簡單總結一下:
- 每個物件在建立的時候,會被賦予一個
__proto__
屬性,它指向建立這個物件的建構函式的prototype
,而prototype
本身也是物件,所以也有自己的__proto__
,這就形成了原型鏈,最終的指向是Object.prototype.__proto__ == null
- 可以通過
new
,Object.create()
,Object.setPrototypeOf()
,直接賦值__proto__
等方法為一個物件指定原型 new
操作符實際做的工作是:建立一個物件,把這個物件作為建構函式的this環境,並把這個物件的原型(proto)指向建構函式的prototype,最後返回這個物件繼承
主要實現的功能是:this指向的繫結,原型鏈的構建- ES6的語法
class
,extends
可以提供更為清晰簡潔的寫法,但是本質上的原理大致相同