從原型到原型鏈
本文主要從構(gòu)造函數(shù) -> 原型(隱式原型+顯示原型)-> 原型鏈的順序,漸進式講解印衔,希望對你有所做幫助啡捶,共勉~
系列文章:
- this指向詳解:思維腦圖與代碼的結(jié)合,讓你一篇搞懂this奸焙、call瞎暑、apply(附面試題)
- 本篇內(nèi)容:請看下方目錄
前言
又到了回憶過去的時候了彤敛,知識就是這樣,原型和原型鏈在我之前的實習生涯中用到的很少——幾乎沒有(噗了赌!我菜我攤牌了)墨榄,但是它和this指向問題一樣,是初級揍拆、中級前端開發(fā)在面試時永遠繞不開的話題渠概。是不是大家每次看面經(jīng)的時候都會去搜索原型相關的知識點?
你看這知識嫂拴,總是在考的時候播揪,才能知道它的重要,就好像曾經(jīng)有一段賊拉真摯的面試題擺在我面前...話題拉回來筒狠,今天我們就收了這個孽障猪狈!
一、構(gòu)造函數(shù)
1.1 什么是構(gòu)造函數(shù)辩恼?
構(gòu)造函數(shù)本身就是一個函數(shù)雇庙,與普通函數(shù)沒有任何區(qū)別,不過為了規(guī)范一般將其首字母大寫。構(gòu)造函數(shù)和普通函數(shù)的區(qū)別在于,使用new生成實例的函數(shù)就是構(gòu)造函數(shù)额各,直接調(diào)用的就是普通函數(shù)华弓。
function Person() {
this.name = 'yuguang';
};
var person = new Person();
console.log(person.name) // yuguang
在這個例子中,Person 就是一個構(gòu)造函數(shù)。
1.2 constructor?
constructor
返回創(chuàng)建實例對象時構(gòu)造函數(shù)的引用。此屬性的值是對函數(shù)本身的引用胸完,而不是一個包含函數(shù)名稱的字符串。
可以看到實例對象的constructor指向了它的構(gòu)造函數(shù)翘贮,而它和原型的關系我們在之后會鏈接到一起赊窥。
1.3 都有哪些數(shù)據(jù)類型或者函數(shù)擁有constructor
呢?
在JavaScript中狸页,每個具有原型的對象都會自動獲得constructor屬性锨能。除了:arguments
、Enumerator
芍耘、Error
腹侣、Global
、Math
齿穗、RegExp
等一些特殊對象之外傲隶,其他所有的JavaScript內(nèi)置對象都具備constructor屬性。例如:Array
窃页、Boolean
跺株、Date
复濒、Function
、Number
乒省、Object
巧颈、String
等。所有主流瀏覽器均支持該屬性袖扛。打開控制臺我們可以驗證一下
// 字符串
console.log('str'.constructor) // ? String() { [native code] }
console.log('str'.constructor === String) // true
// 數(shù)組
console.log([1,2,3].constructor) // ? Array() { [native code] }
console.log([1,2,3].constructor === Array) // true
// 數(shù)字
var num = 1
console.log(num.constructor) // ? Number() { [native code] }
console.log(num.constructor === Number) // true
// Date
console.log(new Date().constructor) // ? Date() { [native code] }
// 注意T曳骸!蛆封!不要混淆哦
console.log(new Date().getTime().constructor) // ? Number() { [native code] }
// Boolean
console.log(true.constructor) // ? Boolean() { [native code] }
console.log(true.constructor === Boolean) // true
// 自定義函數(shù)
function show(){
console.log('yuguang');
};
console.log(show.constructor) // ? Function() { [native code] }
// 自定義構(gòu)造函數(shù)唇礁,無返回值
function Person(){
this.name = name;
};
var p = new Person()
console.log(p.constructor) // ? Person()
// 有返回值
function Person(){
this.name = name;
return {
name: 'yuguang'
}
};
var p = Person()
console.log(p1.constructor) // ? Object() { [native code] }
1.4 模擬實現(xiàn)一個new
既然構(gòu)造函數(shù)與普通函數(shù)的區(qū)別僅僅在于調(diào)用方式上,我們就應該了解new惨篱。
- 當調(diào)用
new
運算符時盏筐,該函數(shù)總會返回一個對象; - 通常情況下砸讳,構(gòu)造器里的this就指向返回的這個對象琢融;
代碼如下:
通常情況下
var MyClass = function(){
this.name = 'yuguang';
};
var obj = new MyClass();
obj.name; // yuguang
特殊情況
var MyClass = function(){
this.name = 'yuguang';
return {
name: '老王'
}
};
var obj = new MyClass();
obj.name // 老王
我們利用 __proto__
(隱式原型,下文會提到)屬性來模擬一下new 調(diào)用構(gòu)造函數(shù)的過程:
var objectNew = function(){
// 從object.prototype上克隆一個空的對象
var obj = new Object();
// 取得外部傳入的構(gòu)造器簿寂,這里是Person
var Constructor = [].shift.call( arguments );
// 更新漾抬,指向正確的原型
obj.__proto__ = Constructor.prototype; //知識點,要考常遂、要考纳令、要考
// 借用外部傳入的構(gòu)造器給obj設置屬性
var ret = Constructor.apply(obj, arguments);
// 確保構(gòu)造器總是返回一個對象
return typeof ref === 'object' ? ret : obj;
}
二、原型
2.1 prototype(顯式原型)
JavaScript 是一種基于原型的語言 (prototype-based language)烈钞,在設計的時候模仿了Java的兩套類型機制:基本類型
和 對象類型
泊碑±ぐ矗可見原型很重要毯欣!
每個對象都擁有一個原型對象,類是以函數(shù)的形式來定義的臭脓。prototype表示該函數(shù)的原型酗钞,也表示一個類的成員的集合±蠢郏看下圖:
可以發(fā)現(xiàn)Person函數(shù)自己的原型都有什么:
-
constructor
(Person.prototype.constructor => Person) -
__proto__
(我們稱它為隱式原型)
此時我們得到了第一張表示構(gòu)造函數(shù)和實例原型之間的關系圖:
那么我們該怎么表示實例與構(gòu)造函數(shù)原型砚作,也就是 person 和 Person.prototype 之間的關系呢,這時候我們就要講到第二個屬性:
2.2 proto(隱式原型)
這是每一個JavaScript對象(除了 null )都具有的一個屬性嘹锁,叫__proto__
葫录,這是一個訪問器屬性(即 getter 函數(shù)和 setter 函數(shù)),通過它可以訪問到對象的內(nèi)部[[Prototype]]
(一個對象或 null )领猾。
function Person() {}
var person = new Person();
console.log(person.__proto__ === Person.prototype); // true
于是我們更新下關系圖:
小結(jié): 每個引用類型的隱式原型都指向它的構(gòu)造函數(shù)的顯式原型
2.3 constructor
前文提到了constructor
米同,它與原型的關系也可以添加到這張圖里骇扇,更新下關系圖:
根據(jù)上圖的關系,下面這段的結(jié)果面粮,大家就一目了然了:
function Person() {}
var person = new Person();
console.log(person.__proto__ == Person.prototype) // true
console.log(Person.prototype.constructor == Person) // true
// 順便學習一個ES5的方法,可以獲得對象的原型
console.log(Object.getPrototypeOf(person) === Person.prototype) // true
接下來我們要繼續(xù)思考實例和原型的關系:
三少孝、實例與原型
當讀取實例的屬性時,如果找不到熬苍,就會查找與對象關聯(lián)的原型中的屬性稍走,如果還查不到,就去找原型的原型柴底,一直找到最頂層為止婿脸。這樣一個查找過程
舉個例子:
function Person() {}
Person.prototype.name = '老王';
var person = new Person();
person.name = '余光';
console.log(person.name) // 余光
delete person.name;
console.log(person.name) // 老王
在這個例子中,我們給實例對象 person 添加了 name 屬性似枕,當我們打印 person.name 的時候盖淡,結(jié)果自然為 余光(is me)。
描述:
但是當我們刪除了 person 的 name 屬性后凿歼,再次讀取 person.name褪迟,從 person 對象中找不到 name 屬性就會從 person 的原型也就是 person.proto ,也就是 Person.prototype中查找答憔,幸運的是我們找到了 name 屬性味赃,結(jié)果為 老王(這...)
總結(jié):
- 嘗試遍歷
實例a
中的所有屬性,但沒有找到目標屬性虐拓; - 查找
name
屬性的這個請求被委托給該實例a
的構(gòu)造器(A
)的原型心俗,它被a.__proto__
記錄著并且指向A.prototype; - A.prototype存在目標屬性蓉驹,返回他的值城榛;
但是萬一還沒有找到呢?原型的原型又是什么呢态兴?
四狠持、原型的原型
在前面,我們已經(jīng)講了原型也是一個對象瞻润,既然是對象喘垂,我們就可以用最原始的方式創(chuàng)建它,那就是:
var obj = new Object();
obj.name = '余光'
console.log(obj.name) // 余光
其實原型對象就是通過Object
構(gòu)造函數(shù)生成的绍撞,結(jié)合之前所講正勒,實例的 __proto__
指向構(gòu)造函數(shù)的 prototype
,可以理解成傻铣,Object.prototype()
是所有對象的根對象章贞,所以我們再次更新下關系圖:
五、原型鏈
每個對象擁有一個原型對象非洲,通過 __proto__
指針指向上一個原型 鸭限,并從中繼承方法和屬性就谜,同時原型對象也可能擁有原型,這樣一層一層里覆,最終指向 null
丧荐。這種關系被稱為原型鏈 (prototype chain),通過原型鏈一個對象會擁有定義在其他對象中的屬性和方法喧枷。
這個鏈條存在著終點虹统,是因為:Object.prototype 的原型是——null,引用阮一峰老師的 《undefined與null的區(qū)別》 就是:
null 表示“沒有對象”隧甚,即該處不應該有值车荔。這句話也意味著 Object.prototype 沒有原型
我們最后更新一次關系圖,藍色線條就可以表示原型鏈這種關系戚扳。
補充忧便,易錯點
1.constructor
首先是 constructor 屬性,我們看個例子:
function Person() {}
var person = new Person();
console.log(person.constructor === Person); // true
當獲取 person.constructor 時帽借,其實 person 中并沒有 constructor 屬性,當不能讀取到constructor 屬性時珠增,會從 person 的原型也就是 Person.prototype 中讀取,正好原型中有該屬性砍艾,所以:
person.constructor === Person.prototype.constructor
2.__proto__
其次是 proto 蒂教,絕大部分瀏覽器都支持這個非標準的方法訪問原型,然而它并不存在于 Person.prototype 中脆荷,實際上凝垛,它是來自于 Object.prototype ,與其說是一個屬性蜓谋,不如說是一個 getter/setter梦皮,當使用 obj.proto 時,可以理解成返回了 Object.getPrototypeOf(obj)桃焕。
3.真的是繼承嗎剑肯?
最后是關于繼承,前面我們講到“每一個對象都會從原型‘繼承’屬性”覆旭,實際上退子,繼承是一個十分具有迷惑性的說法岖妄,引用《你不知道的JavaScript》中的話型将,就是:
繼承意味著復制操作,然而 JavaScript 默認并不會復制對象的屬性荐虐,相反七兜,JavaScript 只是在兩個對象之間創(chuàng)建一個關聯(lián),這樣福扬,一個對象就可以通過委托訪問另一個對象的屬性和函數(shù)腕铸,所以與其叫繼承惜犀,委托的說法反而更準確些。
總結(jié)
- 使用new生成實例的函數(shù)就是構(gòu)造函數(shù)狠裹,直接調(diào)用的就是普通函數(shù)虽界;
- 每個對象都擁有一個原型對象;
- 每個引用類型的隱式原型都指向它的構(gòu)造函數(shù)的顯式原型涛菠;
- Object.prototype是所有對象的根對象莉御;
- 原型鏈存在終點,不會無限查找下去俗冻;
參考
- 《JavaScript設計模式與開發(fā)實踐》
- 木易楊前端進階
- JavaScript深入之從原型到原型鏈
這是我基礎進階的第二篇文章礁叔,如果對您有幫助,不妨收藏迄薄、點贊琅关、評論一下吧。同時本文章和我的其他文章都記錄在前端進階筆記上讥蔽,如果感興趣涣易,不妨點個star,防走丟哦~
關于我:
- 花名:余光, 一名工作不到一年的前端小白
- JavaScript版LeetCode題解
- 前端進階筆記
希望得到您的建議冶伞,如:
- 關系圖的方式是否有幫助到你去理解知識點都毒?
- 文章代碼示例是否有瑕疵?
- 下一篇文章還要不要表情包了??~碰缔?
- ...