引言
最近比較忙導(dǎo)致這篇拖了好久啊,第二篇的作用域和閉包因?yàn)槠渲幸徊糠譀](méi)搞得很清楚也很難受,決定不和自己鉆牛角尖了,本篇最后的面試題部分會(huì)包含一部分閉包的知識(shí)點(diǎn)以彌補(bǔ)上篇沒(méi)講清和講的不夠詳細(xì)的知識(shí)點(diǎn)。
本篇對(duì)標(biāo)犀牛書(shū)第6大章和第9大章
為什么叫大Object,事實(shí)上JS將它單獨(dú)作為一個(gè)基本數(shù)據(jù)類型應(yīng)該就足以稱之為大了,也夠復(fù)雜。撰寫(xiě)本篇的初心還是想搞清楚原型和原型鏈所以放在最前面,只是在看的過(guò)程中發(fā)現(xiàn)和相關(guān)的知識(shí)點(diǎn)很成體系以及也比較重要,所以都總結(jié)記錄了一下,可以作為補(bǔ)充看。
原型和原型鏈
_proto_、prototype和constructor屬性
每個(gè)JS對(duì)象(null除外)都自動(dòng)擁有一個(gè)_proto_屬性,這個(gè)屬性是一個(gè)對(duì)象,指向該對(duì)象的原型
每個(gè)JS函數(shù)(bind()方法除外)都自動(dòng)擁有一個(gè)prototype屬性,這個(gè)屬性也是一個(gè)對(duì)象,用該函數(shù)做構(gòu)造函數(shù)創(chuàng)建的對(duì)象將繼承這個(gè)prototype的屬性,也就是說(shuō)理論上任何一個(gè)JS函數(shù)都可以用作構(gòu)造函數(shù),并且調(diào)用構(gòu)造函數(shù)需要用到prototype屬性。
prototype屬性包含一個(gè)唯一不可枚舉的屬性constructor,這個(gè)屬性是一個(gè)函數(shù)對(duì)象,指向該函數(shù)的構(gòu)造函數(shù)。
看完上述兩點(diǎn)你可能會(huì)認(rèn)為_proto_是不是就是對(duì)象的原型,prototype是不是就是函數(shù)的原型呢? 事實(shí)上并不是,但我們可以說(shuō)對(duì)象的_proto_屬性指向它的原型,構(gòu)造函數(shù)的prototype屬性指向調(diào)用構(gòu)造函數(shù)創(chuàng)建的實(shí)例的原型。
這么說(shuō)有點(diǎn)繞,用圖片(來(lái)源:https://github.com/mqyqingfeng/Blog/issues/2 侵刪)表示即為:

綜上所述,如果我們有一段代碼:
function Person(name, age){
this.name = name;
this.age = age;
}
let person = new Person('xiao hong', 18);
可以得到:
person._proto_ === Person.prototype;
Person.prototype.constructor === Person;
原型鏈
我們知道,當(dāng)執(zhí)行屬性訪問(wèn)表達(dá)式時(shí),首先會(huì)將表達(dá)式的操作主題轉(zhuǎn)化為對(duì)象,然后去對(duì)象中查找屬性,如果找不到就去找與對(duì)象的原型中的屬性,如果還找不到,就繼續(xù)查找原型的原型,直到找到最頂層為止。
那么原型的原型是什么?我們知道,_proto_和prototype屬性也只是普通的對(duì)象而已,既然是對(duì)象,就也有_proto_屬性,一個(gè)普通對(duì)象的_proto_屬性自然指向其構(gòu)造函數(shù)Obejct的prototype屬性,即Object.prototype
那Object.prototype的原型呢?我們可以打印一下:
console.log(Object.prototype._proto_) // null
所以,當(dāng)我們向上查找屬性的時(shí)候,查到Object.prototype就可以停止了,由這些對(duì)象相互關(guān)聯(lián)的原型之間的關(guān)系就是原型鏈。到這里,我們的圖片來(lái)源:https://github.com/mqyqingfeng/Blog/issues/2 侵刪)也可以更新為:

綜上所述,如果我們有一段代碼:
function Person(name, age){
this.name = name;
this.age = age;
}
Person.prototype = {
getName: function(){return this.name}
}
let person = new Person('xiao hong', 18);
person.toString();
我們?cè)趐erson上找不到toString方法,就會(huì)去person的原型上找,person._proto_ === Person.prototype,結(jié)果Person.prototype上也沒(méi)有這個(gè)方法,就會(huì)再去原型的原型上找即Person.prototype._proto_, 要知道Person.prototype._proto_只是一個(gè)普通的對(duì)象,可以被原始的new Object()創(chuàng)建,所以Person.prototype._proto === Object.prototype,所幸Object.prototype上有toString()方法,因此調(diào)用它,查找到此結(jié)束。
補(bǔ)充
-
constructor屬性
不是每個(gè)對(duì)象都有constructor屬性,因此不是每個(gè)對(duì)象都可以用作構(gòu)造函數(shù)的prototype屬性對(duì)象,但可以顯示的定義constructor屬性反向引用構(gòu)造函數(shù)來(lái)修正這個(gè)問(wèn)題。
constructor屬性也是普通的對(duì)象屬性,如果找不到改屬性,也會(huì)從對(duì)象原型上繼續(xù)尋找。如下:
function Person() {
}
var person = new Person();
console.log(person.constructor === Person); // true
當(dāng)獲取 person.constructor 時(shí),其實(shí) person 中并沒(méi)有 constructor 屬性,當(dāng)不能讀取到constructor 屬性時(shí),會(huì)從 person 的原型也就是 Person.prototype 中讀取,正好原型中有該屬性,所以:
person.constructor === Person.prototype.constructor
_proto_
其次是 __proto__ ,絕大部分瀏覽器都支持這個(gè)非標(biāo)準(zhǔn)的方法訪問(wèn)原型,然而它并不存在于Person.prototype中,實(shí)際上,它是來(lái)自于 Object.prototype ,與其說(shuō)是一個(gè)屬性,不如說(shuō)是一個(gè) getter/setter,當(dāng)使用obj.__proto__ 時(shí),可以理解成返回了 Object.getPrototypeOf(obj)。
Function._proto_ === Function.prototype
這里并不是因?yàn)?code>Function是對(duì)象,有_proto_屬性,Function又是函數(shù),函數(shù)對(duì)象的原型指向其構(gòu)造函數(shù)Function.prototype,這樣理解雖然看上去很正確但是是不對(duì)的。引用大佬的話:
Function.prototype是引擎創(chuàng)造出來(lái)的對(duì)象,一開(kāi)始就有了,又因?yàn)槠渌臉?gòu)造函數(shù)都可以通過(guò)原型鏈找到Function.prototype,F(xiàn)unction本身也是一個(gè)構(gòu)造函數(shù),為了不產(chǎn)生混亂,就將這兩個(gè)聯(lián)系到一起了
Object.__proto__ === Function.prototype
Object是對(duì)象的構(gòu)造函數(shù),那么它也是一個(gè)函數(shù),當(dāng)然它的__proto__也是指向Function.prototype
實(shí)際上原型原型鏈就是這樣,但如果你覺(jué)得上述還是很難理解,最好再理解一系列的概念,以下內(nèi)容可以作為作為對(duì)原型和繼承的補(bǔ)充理解:
對(duì)象創(chuàng)建與構(gòu)造函數(shù)
對(duì)象的三種創(chuàng)建方法:
對(duì)象直接量
例如let o = {},對(duì)象直接量是一個(gè)表達(dá)式,這個(gè)表達(dá)式的每次運(yùn)算都會(huì)創(chuàng)建并初始化一個(gè)新的對(duì)象,也就是說(shuō)在一個(gè)函數(shù)中使用對(duì)象直接量會(huì)函數(shù)重復(fù)調(diào)用時(shí)創(chuàng)建很多新對(duì)象
new+構(gòu)造函數(shù)創(chuàng)建對(duì)象
例如let o = new Object()
- 其中
new關(guān)鍵字做了什么呢,根據(jù)MDN的介紹
[圖片上傳失敗...(image-b10ea9-1572447723056)]其中第2點(diǎn)即,設(shè)置創(chuàng)建的新對(duì)象的原型與構(gòu)造函數(shù)的prototype屬性相關(guān)聯(lián)。
注意:new關(guān)鍵字也不是一定創(chuàng)建一個(gè)新對(duì)象的,例如:new Object({}), 根據(jù)ES5規(guī)范,如果new Object(value)中檢測(cè)到Value的類型為object就直接返回該對(duì)象而不會(huì)創(chuàng)建一個(gè)新對(duì)象。
- 其中構(gòu)造函數(shù)做了什么呢,首先明確 構(gòu)造函數(shù)不是一定要和new關(guān)鍵字一起使用的,當(dāng)構(gòu)造函數(shù)做為函數(shù)單獨(dú)調(diào)用時(shí),做了一些不同的事,比如內(nèi)置構(gòu)造函數(shù)如果不和new一起使用則將參數(shù)做一次類型轉(zhuǎn)換,但不一定創(chuàng)建一個(gè)新對(duì)象,至于具體的差別,推薦食用http://yanhaijing.com/es5/#334
補(bǔ)充:new運(yùn)算符優(yōu)先級(jí)
| 優(yōu)先級(jí) | 運(yùn)算類型 | 關(guān)聯(lián)性 | 例子 |
|---|---|---|---|
| 20 | 圓括號(hào) | n/a | (a + b) * c |
| 19 | 成員訪問(wèn) | 從左到右 | object.method |
| 19 | 需要計(jì)算的成員訪問(wèn) | 從左到右 | object[“a”+”b”] |
| 19 | new 帶參數(shù)列表 | n/a | new fun() |
| 19 | 函數(shù)調(diào)用 | 從左到右 | fun() |
| 18 | new 無(wú)參數(shù)列表 | 從右到左 | new fun |
思考:new Foo().getName()和new Foo.getName()兩個(gè)表達(dá)式中的運(yùn)算優(yōu)先級(jí)
我們知道,構(gòu)造函數(shù)沒(méi)有參數(shù)列表的時(shí)候是可以省略括號(hào)的 也就是 new Foo() 等同于 new Foo,根據(jù)上表,優(yōu)先級(jí)越高的先執(zhí)行:
new Foo().getName()表達(dá)式中,new 帶參數(shù)列表優(yōu)先級(jí)高于Foo()函數(shù)調(diào)用表達(dá)式,先執(zhí)行new Foo(),即表達(dá)式等同于(new Foo()).getName()
new Foo.getName()表達(dá)式中,成員訪問(wèn)表達(dá)式優(yōu)先級(jí)高于new 無(wú)帶參列表,即表達(dá)式等同于new (Foo.getName())
Object.create()函數(shù)
該函數(shù)接受兩個(gè)參數(shù),并且返回一個(gè)新創(chuàng)建的對(duì)象,并且將第一個(gè)參數(shù)作為新創(chuàng)建的對(duì)象的原型,甚至可以傳入null來(lái)創(chuàng)建一個(gè)沒(méi)有原型的空對(duì)象,這樣創(chuàng)建的空對(duì)象將不繼承任何基礎(chǔ)方法,比如toString,這意味著這樣創(chuàng)建的對(duì)象將無(wú)法和+一起正常工作。
對(duì)象屬性與屬性繼承
對(duì)象屬性
對(duì)象的三個(gè)屬性 :原型屬性、類屬性、可擴(kuò)展性。
原型
對(duì)象有自有屬性和繼承屬性,其中原型屬性就是作為繼承屬性來(lái)使用的。通過(guò)new創(chuàng)建的對(duì)象用構(gòu)造函數(shù)的prototype屬性作為對(duì)象的原型,通過(guò)Object.create()創(chuàng)建的對(duì)象使用第一個(gè)參數(shù)作為創(chuàng)建對(duì)象的原型,沒(méi)有原型的對(duì)象為數(shù)不多,其中包括Object.prototype和Object.create(null)。
可以用a.isPrototypeOf(b)方法判斷a是否是b原型,即b是否繼承自a.
可以用Object.getPrototypeOf(a)來(lái)獲取a對(duì)象的原型,若a不是對(duì)象類型則拋出類型錯(cuò)誤。
類
通常和構(gòu)造函數(shù)的名稱保持一致,通過(guò)內(nèi)置構(gòu)造函數(shù)創(chuàng)建的對(duì)象有類名,并且可以通過(guò)類似Object.prototype.toString.call(new Date())的方法來(lái)獲得類名,而自定義的對(duì)象沒(méi)有類名,因?yàn)轭悓傩砸欢椤?code>Object”。
擴(kuò)展性
宿主對(duì)象的可擴(kuò)展性由js引擎決定(任何對(duì)象,不是原生對(duì)象就是宿主對(duì)象),ES5中,所有內(nèi)置對(duì)象和自定義對(duì)象都是可擴(kuò)展的。除非將其轉(zhuǎn)換為不可擴(kuò)展的。
可以使用Object.isExtensible()來(lái)檢測(cè)對(duì)象是否可擴(kuò)展,使用Object.preventExtensions()來(lái)將對(duì)象轉(zhuǎn)換為不可擴(kuò)展的。一旦將對(duì)象轉(zhuǎn)換為不可擴(kuò)展的就無(wú)法再轉(zhuǎn)換為可擴(kuò)展的。不可擴(kuò)展只對(duì)于對(duì)象的自有屬性,如果對(duì)象的原型擴(kuò)展了方法那么該對(duì)象將仍然繼承該方法。
補(bǔ)充
存取器屬性getter/setter
如果一個(gè)屬性同時(shí)具有getter/setter方法,則該屬性具有讀/寫(xiě)性,如果只有getter方法,則是只讀屬性,如果只有setter方法,則是只寫(xiě)屬性。
存取器屬性是可繼承的,使用方法如下實(shí)例:
var o = {
x: 1,
get y(){return this.x},
set y(value){this.x = value}
}; //{x: 1, y: 1}
o.y = 2; //{x: 2, y: 2}
var o = {x: 1, get y(){return this.y}}
o.y //RangeError: Maximum call stack size exceeded 讀取器里讀取它自己無(wú)限回調(diào)
var o = {x: 1, set y(value){return value}}
o.y = 3;
o.y // undefined 讀取只寫(xiě)屬性永遠(yuǎn)返回undefined
var o = {x: 1, set y(){return value}} //Setter must have exactly one formal parameter
屬性特性與相關(guān)API總結(jié)
由上述可知,getter/setter存取器屬性與屬性的可讀可寫(xiě)性密切相關(guān),所以可視為屬性的特性。
普通屬性的四個(gè)特性:值、可寫(xiě)性、可枚舉型、可配置性。分別對(duì)應(yīng){value, writable, enumerable, configurable}
讀取器屬性的四個(gè)特性:讀取、寫(xiě)入、可枚舉性、可配置型。分別對(duì)應(yīng){get, set, enmurable, configurable }
可以通過(guò)Object.getOwnPropertyDescriptor({x: 1}, x)查看對(duì)象特定自有屬性特性。如果要查看繼承屬性,需要遍歷原型鏈Object.getPrototypeOf()
可以通過(guò)Object.definedProperty(o, "x", {writable: false})新建或修改某對(duì)象特定自有屬性的特性。對(duì)于新建的屬性來(lái)說(shuō),第三個(gè)參數(shù)中不存在的特性將被描述為false或undefined,對(duì)于修改的屬性來(lái)說(shuō),第三個(gè)參數(shù)中不存在的特性將不會(huì)被修改。
可以通過(guò)Object.seal()將對(duì)象設(shè)置為封閉的,即不可擴(kuò)展的以及將對(duì)象屬性設(shè)置為不可配置的,相對(duì)于Object.preventExtensions()方法,Object.seal()方法處理的對(duì)象將不能刪除和配置已有屬性,但可寫(xiě)屬性依然可以修改。封閉的對(duì)象將不能解封,可以用Object.isSealed()方法檢測(cè)對(duì)象是否是封閉的
可以通過(guò)Object.freeze()方法將對(duì)象凍結(jié),即不可擴(kuò)展的以及將對(duì)象屬性設(shè)置為不可配置的還將所有數(shù)據(jù)屬性設(shè)置為只讀的(對(duì)setter屬性無(wú)效),可以使用Object.isFrozen()檢測(cè)對(duì)象是否凍結(jié)。
面試題練習(xí)
- 寫(xiě)出下面代碼的執(zhí)行結(jié)果
function Foo() {
getName = function() {
console.log(1);
}
return this;
}
Foo.getName = function() {
console.log(2);
}
Foo.prototype.getName = function() {
console.log(3);
}
var getName = function() {
console.log(4);
}
function getName() {
console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
- 寫(xiě)出下面代碼的執(zhí)行結(jié)果
var A = function() {};
A.prototype.n = 1;
var b = new A();
A.prototype = {
n: 2,
m: 3
}
var c = new A();
console.log(b.n);
console.log(b.m);
console.log(c.n);
console.log(c.m);
- 寫(xiě)出下面代碼的執(zhí)行結(jié)果
var F = function() {};
Object.prototype.a = function() {
console.log('a');
};
Function.prototype.b = function() {
console.log('b');
}
var f = new F();
f.a();
f.b();
F.a();
F.b();
- 回答以下兩個(gè)問(wèn)題
function Person(name) {
this.name = name
}
let p = new Person('Tom');
//問(wèn)題1:1. p.__proto__等于什么?
//問(wèn)題2:Person.__proto__等于什么?
- 寫(xiě)出以下代碼的執(zhí)行結(jié)果
var foo = {},
F = function(){};
Object.prototype.a = 'value a';
Function.prototype.b = 'value b';
console.log(foo.a);
console.log(foo.b);
console.log(F.a);
console.log(F.b);
解析
-
本題考察的知識(shí)點(diǎn)很多,包括函數(shù)聲明提前,原型鏈,執(zhí)行上下文this,因此放在了本篇。
Foo.getName() Foo對(duì)象上有g(shù)etName屬性,直接調(diào)用執(zhí)行輸出2
-
getName() 這里考察聲明提前, 函數(shù)聲明提前但函數(shù)定義表達(dá)式不提前,因此這一段實(shí)際被編譯為:
var getName; function getName(){ console.log(5); } getName = function(){ console.log(4); } getName();因此輸出4
Foo().getName() F()給一個(gè)未聲明的變量getName賦值了一個(gè)函數(shù),實(shí)際創(chuàng)建了一個(gè)同名的全局對(duì)象屬性getName并賦值為function(){console.log(1)},又因?yàn)楹瘮?shù)是普通調(diào)用,沒(méi)有綁定在對(duì)象上或?qū)嵗?,返回的this即window,調(diào)用window.getName() 輸出1
Foo()創(chuàng)建的全局getName屬性覆蓋了定義的函數(shù)聲明,輸出1
new Foo.getName() 注意運(yùn)算優(yōu)先級(jí) 先計(jì)算Foo.getName() 輸出 2
new Foo().getName() 注意運(yùn)算優(yōu)先級(jí),先執(zhí)行new Foo(),new Foo()創(chuàng)建一個(gè)新對(duì)象,對(duì)象關(guān)聯(lián)到 Foo.prototype,返回以新創(chuàng)建的對(duì)象為上下文的this,因此調(diào)用Foo.prototype.getName() 輸出3
new new Foo().getName() 執(zhí)行順序new ( (new Foo()).getName()) 同上輸出3
-
本題考察了原型鏈。
var A = function() {}; A.prototype = {constructor: function(){}}; A.prototype = {constructor: function(){}, n: 1}; var b = new A(); b._proto_ = A.prototype; b._proto_ = {constructor: function(){}, n: 1}; A.prototype = { n: 2, m: 3 }; var c = new A(); c._proto_ = A.prototype = { n: 2, m: 3 }; b.n = 1; b.m = undefined; c.n = 2; c.m = 3; -
考察原型鏈
F.prototype = {contructor: function(){}}; Object.prototype.a = function() { console.log('a'); }; Function.prototype.b = function() { console.log('b'); } f._proto_ = F.prototype; f.a => f._proto_.a => F.prototype.a => F.prototype._proto_.a => Object.prototype.a f.a()//'a' f.b => f._proto_.b => F.prototype.b => F.prototype._proto_.b => Object.prototype.b f.b()// TypeError undefined is not a function F.a => F._proto_.a => Function.prototype.a => Function.prototype._proto_.a => Object.prototype.a F.a() // 'a' F.b => F._proto_.b => Function.prototype.b F.b() //'b' -
(1)
p._proto_ = Person.prototype;(2)
Person._proto_ = Function.prototype; 如下:
foo.a => foo._proto_.a => Object.prototype.a => 'value a'
foo.b => foo._proto_.b => Object.prototype.b => undefined
F.a => Foo._proto_.a => Function.prototype.a => Function.prototype._proto_.a => Object.prototype.a => 'value a'
F.b => Foo._proto_.b => Function.prototype.b => 'value b'