【跟著犀牛書(shū)復(fù)習(xí)JS基礎(chǔ)】搞透大Object之對(duì)象原型與原型鏈、對(duì)象創(chuàng)建與構(gòu)造函數(shù)、對(duì)象屬性與屬性繼承、屬性特性與相關(guān)API總結(jié)

引言

最近比較忙導(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_、prototypeconstructor屬性

每個(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 侵刪)表示即為:

實(shí)例原型與構(gòu)造函數(shù)的關(guān)系圖

綜上所述,如果我們有一段代碼:

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 侵刪)也可以更新為:

原型鏈?zhǔn)疽鈭D

綜上所述,如果我們有一段代碼:

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ǔ)充

  1. 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
  1. _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)。

  1. 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)系到一起了

  1. 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.prototypeObject.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ù)中不存在的特性將被描述為falseundefined,對(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í)

  1. 寫(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();
  1. 寫(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);
  1. 寫(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();
  1. 回答以下兩個(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__等于什么?
  1. 寫(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);

解析

  1. 本題考察的知識(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

  2. 本題考察了原型鏈。

    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;
    
  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'
    
  4. (1) p._proto_ = Person.prototype;

    (2)Person._proto_ = Function.prototype;

  5. 如下:

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' 
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容