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
可以提供更為清晰簡潔的寫法,但是本質上的原理大致相同