原型和原型鏈
原型prototype,在建立新函式的時候,會自動生成,而prototype中也會有一個constructor,回指建立該prototype的函式物件。
__proto__是物件或者例項中內建的[[prototype]],其指向的是產生該物件的物件的prototype,在瀏覽器中提供了__proto__讓我們可以訪問,通過__proto__的指向形成的一個鏈條,就稱做原型鏈,原型鏈的整個鏈路是:例項物件- ->建構函式的prototype- ->Object的prototype- ->null。
我們在訪問物件的屬性或者方法的時候,首先從本物件尋找,如果本物件不存在該屬性或者方法時,就會沿著原型鏈向上尋找,直至找到該屬性或者方法,或者到null時停止。
這也解釋了為什麼陣列物件上沒有push,pop,shift,unshift等方法,卻可以訪問。
constructor
constructor屬性指向的是生成該函式(物件)的函式(物件),例如
var a = function(){};
var b = new a();
var c = {};
var d = [];
//以下皆為true
console.log(b.constructor === a) //因為例項b是由建構函式產生的
console.log(a.constructor === Function)//函式a實際是Function的例項,同理
console.log(c.constructor === Object)//空物件c是Object的例項
console.log(d.constructor === Array)//空物件c是Object的例項
console.log(Object.constructor === Function)//Object自身就是一個建構函式,同理
console.log(Array.constructor === Function)//Array自身也是一個建構函式
//---------------------------------------------------------------
//首先__proto__指向的是產生該物件的物件的prototype,
//也即a.prototype,prototype中也的constructor,回指建立該prototype的函式物件,也即函式a
console.log(b.__proto__.constructor === a)
複製程式碼
這裡順便說一下instanceof,**A instanceof B **是在 A 的原型鏈中找 B 的 prototype,找到返回 true,找不到返回 false
//有個奇怪的現象,下面都返回true,這是為什麼呢?
//因為JS中一切都繼承自Object,除了最頂層的null,
//所以在Function的原型鏈中能找到Object.prototype
console.log(Function instanceof Object)
//而Object自身就是一個建構函式,因此在Object的原型鏈中也能找到Function.prototype
console.log(Object instanceof Function)
複製程式碼
通過原型鏈實現繼承
由上面的分析,我們可以利用原型鏈實現繼承的邏輯,繼承是物件導向中的一個很重要的概念
function Dog(name){
this.name = name;
this.say1 = function(){
console.log(this.name)
}
}
Dog.prototype.say2 = function(){
console.log(this.name)
}
Dog.prototype.test = 1
//say本來應該是所有Dog例項的共有方法,
//如果放在建構函式中,那麼就會導致沒辦法資料共享,每一個例項都有自己的屬性和方法的副本,這是對資源的極大浪費
//如果放在Dog.prototype中,那麼利用原型鏈的特性,就可以讓所有例項共用一個方法,
//需要注意的是,由於共用了一個方法,對屬性的更改是對所有例項透明的
var dog1 = new Dog('lalala');
let dog2 = new Dog('wahaha');
dog1.test++;//2
dog2.test++;//3
console.log(dog1.say1 === dog2.say1)// false
console.log(dog1.say2 === dog2.say2)// true
//現在,我們可以嘗試著去實現繼承了
//我們是通過原型鏈去實現繼承的,
//之前的原型鏈是:Dog例項 --> Dog函式 --> Object --> null
//那麼現在的原型鏈需要改成 Dog例項 --> Dog函式 --> Dog父類(Animal函式) --> Object --> null
//第一種方案,改變Dog函式的prototype,讓他指向Animal的例項
function Animal(){
this.species = 'unknown';
}
Dog.prototype = new Animal();
//這裡改變後會導致prototype中的constructor改變
Dog.prototype.constructor = Dog;
//第二鍾方案,改變Dog函式的prototype,讓他指向Animal的prototype
function Animal(){}
Animal.prototype.species = 'unknown';
Dog.prototype = Animal.prototype;
//這裡改變後會導致prototype中的constructor改變
Dog.prototype.constructor = Dog;
//第三種方案,呼叫apply或call,將Animal的this繫結到Dog中
function Animal(){
this.species = 'unknown';
}
function Dog(name){
Animal.apply(this, arguments);
this.name = name;
}
//第四種方法,通過Object.create()方法實現繼承,過濾掉了父類例項屬性,Dog.prototype中就沒有了Animal的例項化資料了
//這種方法也是ES6中Class被babel編譯成ES5所用的方法
function Animal(){
this.species = 'unknown';
}
function Dog(name){
Animal.apply(this, arguments);
this.name = name;
}
//這裡模擬了 Dog.prototype = Object.create(Animal.prototype)
var f = function(){};
f.prototype = Animal.pototype;
Dog.prototype = new f();
Dog.__proto__ = Animal;
//這裡改變後會導致prototype中的constructor改變
Dog.prototype.constructor = Dog;
//現在就能訪問到Animal中的species屬性了
var dog = new Dog('lalala');
dog.species;//unknown
複製程式碼
以上這些就是利用原型鏈實現繼承的一些方法
ES6的class類
有了以上的知識,我們就可以研究一下ES6的class類了,這個語法糖能讓我們更容易的實現類和繼承,其提供了extends,static,super等關鍵字
//這是es6的程式碼實現
class Parent {
static l(){
console.log(222)
}
constructor(m){
this.m = m
}
get(){
return this.m;
}
}
class Child extends Parent {
constructor(n){
super(4);
this.n = n;
}
get(){
return this.n
}
set(a){
this.n = a;
}
}
//這是利用babel編譯之後的es5的實現
//_createClass是一個自執行函式,作用給建構函式繫結靜態方法和動態方法
//對於靜態的static關鍵字宣告的變數,會直接繫結在函式物件上,作為靜態屬性(方法)
//對於在class中宣告的函式方法,則會繫結在建構函式的prototype上,通過Object.definePropety方法
var _createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
//如果父函式沒有返回值或者返回值不為object或者function,則返回子類的this
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return call && (typeof call === "object" || typeof call === "function") ? call : self;
}
//_inherits就是extends關鍵字發揮的作用,實現了繼承的功能。利用&&的短路特性,對superClass做了容錯性處理,然後將子類Object.create()傳了兩個引數,一個引數是父類superClass.prototype,作用在上面解釋繼承的方法時講過了,第二個引數是一個鍵值對,key代表著屬性,value則和Object.definePropety中descriptor一樣,這裡改變constructor的目的,也在解釋繼承時講過了,最後將subClass.__proto__指向superClass
function _inherits(subClass, superClass) {
//...省略
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
enumerable: false,
writable: true,
configurable: true
}
});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
//_classCallCheck是保證建構函式不能被當成普通函式呼叫,需要用new關鍵字
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
var Parent = function () {
_createClass(Parent, null, [{
key: "l",
value: function l() {
console.log(222);
}
}]);
function Parent(m) {
_classCallCheck(this, Parent);
this.m = m;
}
_createClass(Parent, [{
key: "get",
value: function get() {
return this.m;
}
}]);
return Parent;
}();
var Child = function (_Parent) {
_inherits(Child, _Parent);
function Child(n) {
_classCallCheck(this, Child);
//由於在_inherits中將subClass(child).__proto__指向了superClass(Parent),所以這裡即是Parent.call(this,4),即這裡執行的是super函式,super也可以呼叫父類的靜態方法,
//如果父函式沒有返回值或者返回值不為object或者function,則返回子類的this
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, 4));
_this.n = n;
return _this;
}
_createClass(Child, [{
key: "set",
value: function set(a) {
this.n = a;
}
}]);
return Child;
}(Parent);
複製程式碼
總結
- 通過以上分析,對原型和原型鏈有了更加深入和清晰的瞭解,也熟悉了constructor和instanceof的用法,加深了基於原型鏈的繼承方式的瞭解,理清了這塊知識。
- 在對ES6的class通過babel編譯後的原始碼的分析中,也瞭解到了Object.create和Object.setPrototypeOf的用法,挖掘瞭如何去模擬super,extends和static的實現。