【跟着犀牛書複習JS基礎】搞透大Object之對象原型與原型鏈、對象建立與構造函數、對象屬性與屬性繼承、屬性特性與相關API總結

引言javascript

最近比較忙致使這篇拖了很久啊,第二篇的做用域和閉包由於其中一部分沒搞得很清楚也很難受,決定不和本身鑽牛角尖了,本篇最後的面試題部分會包含一部分閉包的知識點以彌補上篇沒講清和講的不夠詳細的知識點。java

本篇對標犀牛書第6大章和第9大章git

爲何叫大Object,事實上JS將它單獨做爲一個基本數據類型應該就足以稱之爲了,也夠複雜。撰寫本篇的初心仍是想搞清楚原型和原型鏈因此放在最前面,只是在看的過程當中發現和相關的知識點很成體系以及也比較重要,因此都總結記錄了一下,能夠做爲補充看。github

原型和原型鏈

_proto_prototypeconstructor屬性

每一個JS對象(null除外)都自動擁有一個_proto_屬性,這個屬性是一個對象,指向該對象的原型面試

每一個JS函數(bind()方法除外)都自動擁有一個prototype屬性,這個屬性也是一個對象,用該函數作構造函數建立的對象將繼承這個prototype的屬性,也就是說理論上任何一個JS函數均可以用做構造函數,而且調用構造函數須要用到prototype屬性。瀏覽器

prototype屬性包含一個惟一不可枚舉的屬性constructor,這個屬性是一個函數對象,指向該函數的構造函數。閉包

看完上述兩點你可能會認爲_proto_是否是就是對象的原型,prototype是否是就是函數的原型呢? 事實上並非,但咱們能夠說對象的_proto_屬性指向它的原型,構造函數的prototype屬性指向調用構造函數建立的實例的原型。函數

這麼說有點繞,用圖片(來源:github.com/mqyqingfeng… 侵刪)表示即爲:ui

實例原型與構造函數的關係圖

綜上所述,若是咱們有一段代碼:this

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;
複製代碼

原型鏈

咱們知道,當執行屬性訪問表達式時,首先會將表達式的操做主題轉化爲對象,而後去對象中查找屬性,若是找不到就去找與對象的原型中的屬性,若是還找不到,就繼續查找原型的原型,直到找到最頂層爲止。

那麼原型的原型是什麼?咱們知道,_proto_prototype屬性也只是普通的對象而已,既然是對象,就也有_proto_屬性,一個普通對象的_proto_屬性天然指向其構造函數Obejct的prototype屬性,即Object.prototype

Object.prototype的原型呢?咱們能夠打印一下:

console.log(Object.prototype._proto_) // null
複製代碼

因此,當咱們向上查找屬性的時候,查到Object.prototype就能夠中止了,由這些對象相互關聯的原型之間的關係就是原型鏈。到這裏,咱們的圖片來源:github.com/mqyqingfeng… 侵刪)也能夠更新爲:

原型鏈示意圖

綜上所述,若是咱們有一段代碼:

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();
複製代碼

咱們在person上找不到toString方法,就會去person的原型上找,person._proto_ === Person.prototype,結果Person.prototype上也沒有這個方法,就會再去原型的原型上找即Person.prototype._proto_, 要知道Person.prototype._proto_只是一個普通的對象,能夠被原始的new Object()建立,因此Person.prototype._proto === Object.prototype,所幸Object.prototype上有toString()方法,所以調用它,查找到此結束。

補充

  1. constructor屬性

不是每一個對象都有constructor屬性,所以不是每一個對象均可以用做構造函數的prototype屬性對象,但能夠顯示的定義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
複製代碼
  1. _proto_

其次是 __proto__ ,絕大部分瀏覽器都支持這個非標準的方法訪問原型,然而它並不存在於Person.prototype中,實際上,它是來自於 Object.prototype ,與其說是一個屬性,不如說是一個 getter/setter,當使用obj.__proto__ 時,能夠理解成返回了 Object.getPrototypeOf(obj)

  1. Function._proto_ === Function.prototype

這裏並非由於Function是對象,有_proto_屬性,Function又是函數,函數對象的原型指向其構造函數Function.prototype,這樣理解雖然看上去很正確可是是不對的。引用大佬的話:

Function.prototype是引擎創造出來的對象,一開始就有了,又由於其餘的構造函數均可以經過原型鏈找到Function.prototype,Function自己也是一個構造函數,爲了避免產生混亂,就將這兩個聯繫到一塊兒了

  1. Object.__proto__ === Function.prototype

Object是對象的構造函數,那麼它也是一個函數,固然它的__proto__也是指向Function.prototype

實際上原型原型鏈就是這樣,但若是你以爲上述仍是很難理解,最好再理解一系列的概念,如下內容能夠做爲做爲對原型和繼承的補充理解:

對象建立與構造函數

對象的三種建立方法:

對象直接量

例如let o = {},對象直接量是一個表達式,這個表達式的每次運算都會建立並初始化一個新的對象,也就是說在一個函數中使用對象直接量會函數重複調用時建立不少新對象

new+構造函數建立對象

例如let o = new Object()

  • 其中new關鍵字作了什麼呢,根據MDN的介紹

![image-20191026185858754](/Users/chenxingjian/Library/Application Support/typora-user-images/image-20191026185858754.png)其中第2點即,設置建立的新對象的原型與構造函數的prototype屬性相關聯。

注意:new關鍵字也不是必定建立一個新對象的,例如:new Object({}), 根據ES5規範,若是new Object(value)中檢測到Value的類型爲object就直接返回該對象而不會建立一個新對象。

  • 其中構造函數作了什麼呢,首先明確 構造函數不是必定要和new關鍵字一塊兒使用的,當構造函數作爲函數單獨調用時,作了一些不一樣的事,好比內置構造函數若是不和new一塊兒使用則將參數作一次類型轉換,但不必定建立一個新對象,至於具體的差異,推薦食用http://yanhaijing.com/es5/#334

補充:new運算符優先級

優先級 運算類型 關聯性 例子
20 圓括號 n/a (a + b) * c
19 成員訪問 從左到右 object.method
19 須要計算的成員訪問 從左到右 object[「a」+」b」]
19 new 帶參數列表 n/a new fun()
19 函數調用 從左到右 fun()
18 new 無參數列表 從右到左 new fun

思考:new Foo().getName()new Foo.getName()兩個表達式中的運算優先級

咱們知道,構造函數沒有參數列表的時候是能夠省略括號的 也就是 new Foo() 等同於 new Foo,根據上表,優先級越高的先執行:

new Foo().getName()表達式中,new 帶參數列表優先級高於Foo()函數調用表達式,先執行new Foo(),即表達式等同於(new Foo()).getName()

new Foo.getName()表達式中,成員訪問表達式優先級高於new 無帶參列表,即表達式等同於new (Foo.getName())

Object.create()函數

該函數接受兩個參數,而且返回一個新建立的對象,而且將第一個參數做爲新建立的對象的原型,甚至能夠傳入null來建立一個沒有原型的空對象,這樣建立的空對象將不繼承任何基礎方法,好比toString,這意味着這樣建立的對象將沒法和+一塊兒正常工做。

對象屬性與屬性繼承

對象屬性

對象的三個屬性 :原型屬性、類屬性、可擴展性。

原型

對象有自有屬性和繼承屬性,其中原型屬性就是做爲繼承屬性來使用的。經過new建立的對象用構造函數的prototype屬性做爲對象的原型,經過Object.create()建立的對象使用第一個參數做爲建立對象的原型,沒有原型的對象爲數很少,其中包括Object.prototypeObject.create(null)

能夠用a.isPrototypeOf(b)方法判斷a是不是b原型,即b是否繼承自a.

能夠用Object.getPrototypeOf(a)來獲取a對象的原型,若a不是對象類型則拋出類型錯誤。

一般和構造函數的名稱保持一致,經過內置構造函數建立的對象有類名,而且能夠經過相似Object.prototype.toString.call(new Date())的方法來得到類名,而自定義的對象沒有類名,由於類屬性必定爲「Object」。

擴展性

宿主對象的可擴展性由js引擎決定(任何對象,不是原生對象就是宿主對象),ES5中,全部內置對象和自定義對象都是可擴展的。除非將其轉換爲不可擴展的。

可使用Object.isExtensible()來檢測對象是否可擴展,使用Object.preventExtensions()來將對象轉換爲不可擴展的。一旦將對象轉換爲不可擴展的就沒法再轉換爲可擴展的。不可擴展只對於對象的自有屬性,若是對象的原型擴展了方法那麼該對象將仍然繼承該方法。

補充

存取器屬性getter/setter

若是一個屬性同時具備getter/setter方法,則該屬性具備讀/寫性,若是隻有getter方法,則是隻讀屬性,若是隻有setter方法,則是隻寫屬性。

存取器屬性是可繼承的,使用方法以下實例:

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 讀取器裏讀取它本身無限回調

var o = {x: 1, set y(value){return value}}
o.y = 3;
o.y // undefined 讀取只寫屬性永遠返回undefined

var o = {x: 1, set y(){return value}} //Setter must have exactly one formal parameter
複製代碼

屬性特性與相關API總結

由上述可知,getter/setter存取器屬性與屬性的可讀可寫性密切相關,因此可視爲屬性的特性。

普通屬性的四個特性:值、可寫性、可枚舉型、可配置性。分別對應{value, writable, enumerable, configurable}

讀取器屬性的四個特性:讀取、寫入、可枚舉性、可配置型。分別對應{get, set, enmurable, configurable }

能夠經過Object.getOwnPropertyDescriptor({x: 1}, x)查看對象特定自有屬性特性。若是要查看繼承屬性,須要遍歷原型鏈Object.getPrototypeOf()

能夠經過Object.definedProperty(o, "x", {writable: false})新建或修改某對象特定自有屬性的特性。對於新建的屬性來講,第三個參數中不存在的特性將被描述爲falseundefined,對於修改的屬性來講,第三個參數中不存在的特性將不會被修改。

能夠經過Object.seal()將對象設置爲封閉的,即不可擴展的以及將對象屬性設置爲不可配置的,相對於Object.preventExtensions()方法,Object.seal()方法處理的對象將不能刪除和配置已有屬性,但可寫屬性依然能夠修改。封閉的對象將不能解封,能夠用Object.isSealed()方法檢測對象是不是封閉的

能夠經過Object.freeze()方法將對象凍結,即不可擴展的以及將對象屬性設置爲不可配置的還將全部數據屬性設置爲只讀的(對setter屬性無效),可使用Object.isFrozen()檢測對象是否凍結。

面試題練習

  1. 寫出下面代碼的執行結果
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. 寫出下面代碼的執行結果
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. 寫出下面代碼的執行結果
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. 回答如下兩個問題
function Person(name) {
    this.name = name
}
let p = new Person('Tom');

//問題1:1. p.__proto__等於什麼?

//問題2:Person.__proto__等於什麼?
複製代碼
  1. 寫出如下代碼的執行結果
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. 本題考察的知識點不少,包括函數聲明提早,原型鏈,執行上下文this,所以放在了本篇。

    • Foo.getName() Foo對象上有getName屬性,直接調用執行輸出2

    • getName() 這裏考察聲明提早, 函數聲明提早但函數定義表達式不提早,所以這一段實際被編譯爲:

      var getName;
      function getName(){
        console.log(5);
      }
      getName = function(){
        console.log(4);
      }
      getName();
      複製代碼

      所以輸出4

    • Foo().getName() F()給一個未聲明的變量getName賦值了一個函數,實際建立了一個同名的全局對象屬性getName並賦值爲function(){console.log(1)},又由於函數是普通調用,沒有綁定在對象上或實例上,返回的this即window,調用window.getName() 輸出1

    • Foo()建立的全局getName屬性覆蓋了定義的函數聲明,輸出1

    • new Foo.getName() 注意運算優先級 先計算Foo.getName() 輸出 2

    • new Foo().getName() 注意運算優先級,先執行new Foo(),new Foo()建立一個新對象,對象關聯到 Foo.prototype,返回以新建立的對象爲上下文的this,所以調用Foo.prototype.getName() 輸出3

    • new new Foo().getName() 執行順序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' 
複製代碼
相關文章
相關標籤/搜索