構造函數
就是提供一個生成對象的模板並描述對象基本結構的函數。一個構造函數能夠生成多個對象,這些對象都有相同的結構css
構造函數
自己就是一個函數,不過爲了規範通常將其首字母大寫。構造函數
和 普通函數
的區別在於使用 new
生成實例的函數就是構造函數,直接調用的就是普通函數express
生成對象實例時必須使用 new
命令來調用構造函數,因此構造函數更合理的理解應該是 函數的構造調用
json
constructor
返回建立實例對象時構造函數的引用,此屬性的值是對函數自己的引用,而不是一個包含函數名稱的字符串數組
function Person(age) {
this.age = age;
}
var p = new Person(18);
p.constructor === Person; // true
p.constructor === Object; // false
複製代碼
那普通函數建立的實例是否是必定沒有 constructor
屬性呢?不必定瀏覽器
// 普通函數
function person(age) {
this.age = age;
}
var p = person(20); // undefined
p.constructor; // Cannot read property 'constructor' of undefined
// 普通函數
function person(age) {
return {
age: age
}
}
var p = person(20);
p.constructor === Object; // true
複製代碼
MDN
是這樣介紹的安全
The Symbol() function returns a value of type symbol, has static properties that expose several members of built-in objects, has static methods that expose the global symbol registry, and resembles a built-in object class but is incomplete as a constructor because it does not support the syntax "new Symbol()"markdown
Symbol
是基本數據類型,但做爲構造函數來講它並不完整,由於它不支持語法 new Symbol()
,所以認爲其不是構造函數,若要生成實例直接使用 Symbol()
便可(來自 MDN
),每一個從 Symbol()
返回的值都是惟一的cookie
new Symbol(1); // Symbol is not a constructor
Symbol(1); // Symbol(1)
複製代碼
雖然是基本數據類型,但 Symbol(1)
實例能夠獲取 constructor
屬性值數據結構
var a = Symbol(1); // Symbol(1)
console.log(a.constructor); // ƒ Symbol() { [native code] }
複製代碼
這裏的 constructor
屬性實際上是 Symbol
原型上的,即 Symbol.prototype.constructor
返回建立實例原型的函數, 默認爲 Symbol
函數閉包
對於引用類型來講 constructor
屬性值是能夠修改的,但對於基本類型來講是隻讀的
引用類型狀況其值可修改這個很好理解,好比原型鏈繼承方案中就須要對 constructor
從新賦值進行修正
function Foo() {
this.value = 42;
}
Foo.prototype = {
method: function() {}
};
function Bar() {}
// 設置 Bar 的 prototype 屬性爲 Foo 的實例對象
Bar.prototype = new Foo();
Bar.prototype.foo = 'Hello JS';
Bar.prototype.constructor === Object; // true
var test = new Bar() // 建立 Bar 的一個新實例
console.log(test);
複製代碼
// 修正 Bar.prototype.constructor 爲 Bar 自己
Bar.prototype.constructor = Bar;
var test = new Bar() // 建立 Bar 的一個新實例
console.log(test);
複製代碼
對於基本類型來講是隻讀的,如 1
、"1"
、true
、Symbol
(null
和 undefined
是沒有 constructor
屬性的)
function Type() {};
const types = [1, "1", true, Symbol(1)];
for(let i = 0, len = types.length; i < len; i ++) {
types[i].constructor = Type;
types[i] = [types[i].constructor, types[i] instanceof Type, types[i].toString()];
};
console.log(types.join("\n"));
複製代碼
爲何呢?由於建立它們的是隻讀的原生構造函數 native constructors
,這個例子也說明了依賴一個對象的 constructor
屬性並不安全
new 運算符建立一個用戶定義的對象類型的實例或具備構造函數的內置對象的實例。 ——(來自於MDN)
當代碼 new Foo(...) 執行時,會發生如下事情:
Foo.prototype
的新對象被建立Foo
並將 this
綁定到新建立的對象上(new Foo
等同於 new Foo()
,即沒有指定參數列表,Foo
不帶任何參數調用的狀況)new
表達式的結果,若構造函數沒有顯式返回一個對象,則使用步驟 1 建立的對象// 初版
function createNew() {
// 建立一個空的對象
let obj = new Object();
// 得到構造函數,arguments 中去除第一個參數
const Con = [].shift.call(arguments);
// 連接到原型
// 建立一個原型爲構造器的 prototype 的空對象 obj
// const obj = Object.create(constructor.prototype);
obj.__proto__ = Con.prototype;
// 綁定 this 實現繼承,使用 apply 改變構造函數 this 的指向到新建的對象,這樣 obj 就能夠訪問到構造函數中的屬性
Con.apply(obj, arguments);
// 返回對象
return obj;
};
複製代碼
測試一下
function Car(color) {
this.color = color;
}
Car.prototype.start = function() {
console.log(this.color + " car start");
}
var car = createNew(Car, "black");
car.color;
// black
car.start();
// black car start
複製代碼
上面的代碼已經實現了 80%,如今繼續優化。構造函數返回值有以下三種狀況:
this
失效,實例 person 中只能訪問到返回對象中的屬性
function Person(age, name) {
this.age = age;
return {
name: name
}
}
const person = new Person(18, "tn");
person.age; // undefined
person.name; // "tn"
複製代碼
實例 person 中只能訪問到構造函數中的屬性,和上面徹底相反
function Person(age, name) {
this.age = age;
}
const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
複製代碼
實例 person 中只能訪問到構造函數中的屬性,和狀況 1 相反,結果至關於沒有返回值
function Person(age, name) {
this.age = age;
return "new person";
}
const person = new Person(18, "tn");
person.age; // 18
person.name; // undefined
複製代碼
因此須要判斷下返回值是否是一個對象,如果對象則返回該對象,否則返回新建立的 obj 對象,實現代碼以下
// 第二版
function createNew() {
// 建立一個空的對象
let obj = new Object();
// 得到構造函數,arguments 中去除第一個參數
const Con = [].shift.call(arguments);
// 連接到原型,obj 能夠訪問到構造函數原型中的屬性
obj.__proto__ = Con.prototype;
// 綁定 this 實現繼承,obj 能夠訪問到構造函數中的屬性
const ret = Con.apply(obj, arguments);
// 優先返回構造函數返回的對象
return ret instanceof Object ? ret : obj;
};
複製代碼
常常看到一句話說:萬物皆對象
,對象就是屬性的集合(對象裏面的一切都是屬性,只有屬性沒有方法,方法也是一種屬性,由於它的屬性表示爲鍵值對的形式)。而在 JavaScript 中,建立對象有幾種方式,如對象字面量
、經過構造函數 new
一個對象、Object.create()
暫且先無論上面的代碼有什麼意義,至少能看出都是對象且卻存在着差別性
其實在 JavaScript 中能夠將對象分爲函數對象
和普通對象
函數對象
就是 JavaScript 中用函數來模擬的類實現,如 Object
、 Function
就是典型的函數對象下述代碼中 obj一、obj二、obj三、obj4 都是普通對象,fun一、fun二、fun3 都是 Function 的實例,即函數對象
function fun1() {};
const fun2 = function() {};
const fun3 = new Function('name','console.log(name)');
const obj1 = {};
const obj2 = new Object();
const obj3 = new fun1();
const obj4 = new new Function();
console.log(typeof Object); // function
console.log(typeof Function); // function
console.log(typeof fun1); // function
console.log(typeof fun2); // function
console.log(typeof fun3); // function
console.log(typeof obj1); // object
console.log(typeof obj2); // object
console.log(typeof obj3); // object
console.log(typeof obj4); // object
複製代碼
所以,全部 Function
的實例都是函數對象,其餘均爲普通對象,其中包括 Function
實例的實例
function Foo() {}
// 這個函數是 Function 的實例對象
// function 就是一個語法糖
// 內部調用了 new Function(...)
複製代碼
JavaScript 中萬物皆對象,而對象皆出自構造(構造函數)
個人理解是全部對象都是由 new
操做符後跟函數調用來建立的,字面量表示法只是語法糖(即本質也是 new
,功能不變、使用更簡潔),不管是 function Foo()
仍是 let a = { b : 1 }
對於建立一個對象來講,更推薦使用字面量的方式建立,由於使用 new Object()
的方式建立對象須要經過做用域鏈一層層找到 Object
,可是使用字面量的方式就沒這個問題
function Foo() {};
// function 就是個語法糖
// 內部等同於 new Function()
let a = { b: 1 };
// 這個字面量內部也是使用了 new Object()
複製代碼
Number
、String
、Boolean
、Array
、Object
、Function
、Date
、RegExp
、Error
等都是函數,並且是內置的原生構造函數,在運行時會自動出如今執行環境中
構造函數是爲了建立特定類型的對象,這些經過同一構造函數建立的對象有相同原型,共享某些方法。如:全部的數組均可以調用 push
方法,由於它們有相同原型
原型和原型鏈都是來源於對象而服務於對象的概念
JavaScript 常被描述爲一種基於原型的語言 (prototype-based language),這個和 Java 等基於類的語言不同
每一個對象擁有一個原型對象,對象以其原型爲模板,從原型繼承方法和屬性,這些屬性和方法定義在對象的構造器函數的 prototype
屬性上,而非對象實例自己
prototype - object that provides shared properties for other objects
在規範裏,prototype
被定義爲:給其它對象實例提供共享屬性的對象。所以 prototype
本身自己也是對象,只是被用以承擔某個職能罷了
只有函數才擁有該屬性,它是
function
對象的一個顯式原型屬性,當聲明一個函數時該屬性就被自動建立了,它定義了構造函數製造出來的對象實例的公共祖先,經過該構造函數產生的對象能夠繼承該原型上的屬性和方法
基本上全部函數都有這個屬性,可是也有一個例外,若用如下方法建立一個函數,可發現這個函數是不具備 prototype
屬性。由於 Function.prototype
是引擎建立出來的函數對象,引擎認爲不須要給這個對象添加 prototype
屬性
let fun = Function.prototype.bind();
複製代碼
在 prototype
上面添加屬性和方法,每一個構造出來的對象實例均可繼承這些屬性和方法。雖然每一個對象都是獨立的,但它們都有共同的祖先,當訪問這個對象的屬性時,若對象自己沒有該屬性,則會往上找到它的原型,而後在原型上訪問這個屬性
prototype
有個默認屬性 constructor
,指向一個函數,這個函數就是該對象的構造函數
Person.prototype.constructor === Person // true
複製代碼
constructor
是個公有且不可枚舉屬性,一旦改變了函數的 prototype
,那新對象就沒有這個屬性(可經過原型鏈取到 constructor
)
注意,每一個對象都有其對應的構造函數,自己或者繼承而來。單從constructor
這個屬性來說只有 prototype
對象纔有。每一個函數在建立時 JavaScript 會同時建立一個該函數對應的 prototype
對象
函數建立的對象.__proto__ === 該函數.prototype
函數.prototype.constructor === 該函數自己
故經過函數建立的對象即便本身沒有 constructor
屬性,它也能經過 __proto__
找到對應的constructor
,因此任何對象最終均可以找到其對應的構造函數
其實這個屬性能夠說是一個歷史遺留問題,它有兩個做用
xx.constructor.method
來擴展__proto__
一、首先須要明確:
__proto__
和constructor
是對象獨有的;prototype
是函數獨有的
二、但在 JavaScript 中,函數也是對象,所以函數也擁有__proto__
和constructor
屬性
每一個對象都有該隱式原型屬性
,指向了原型(如果構造函數建立的對象,則指向建立該對象的構造函數的原型)
這裏用 __proto__
獲取對象的原型,__proto__
是每一個對象實例上都有的屬性,prototype
是構造函數的屬性,這兩個並不同,但 __proto__
和 prototype
指向同一個對象
__proto__
指向了 [[prototype]](一個對象或 null)
,因 [[prototype]]
是內部屬性,並不能從外部訪問到,所以有些瀏覽器實現了 __proto__
來訪問
所以,ECMAScript 規範說 prototype
應當是一個隱式引用:
Object.getPrototypeOf(obj)
訪問指定對象的 prototype
對象Object.setPrototypeOf(obj, anotherObj)
設置指定對象的 prototype
對象__proto__
,使得能夠經過 obj.__proto__
直接訪問原型,經過 obj.__proto__ = anotherObj
直接設置原型__proto__
屬性歸入了規範的一部分,以確保 Web 瀏覽器的兼容性__proto__
屬性既不能被 for...in
遍歷出來,也不能被 Object.keys(obj)
查找出來
其實 __proto__
是個定義在 Object.prototype
上的訪問器屬性,即用 getter
和 setter
定義的屬性,訪問對象的 obj.__proto__
屬性,默認走的是 Object.prototype
對象上 __proto__
屬性的 get/set
方法
Object.defineProperty(Object.prototype,'__proto__',{
get() {
console.log('get')
}
});
({}).__proto__;
console.log((new Object()).__proto__);
// get
// get
const weakMap = new WeakMap();
Object.prototype = {
get __proto__() {
return this['[[prototype]]'] === null ? weakMap.get(this) : this['[[prototype]]'];
},
set __proto__(newPrototype) {
if (!Object.isExtensible(newPrototype)) throw new TypeError(`${newPrototype} is not extensible`);
const isObject = typeof newPrototype === 'object' || typeof newPrototype === 'function';
if (newPrototype === null || isObject) {
// 若是以前經過 __proto__ 設置成 null
// 此時再經過給 __proto__ 賦值的方式修改原型都是徒勞
// 表現就是 obj.__proto__ = { a: 1 } 就像一個普通屬性 obj.xxx = { a: 1 }
if (this['[[prototype]]'] === null) {
weakMap.set(this, newPrototype);
} else {
this['[[prototype]]'] = newPrototype;
}
}
},
// ... 其它屬性如 toString,hasOwnProperty 等
};
複製代碼
__proto__
屬性在 ES6 時被標準化,以確保 Web 瀏覽器的兼容性,可是不推薦使用,除了標準化的緣由以外還有性能問題,爲了更好的支持,推薦使用 Object.getPrototypeOf()
若一個對象的 __proto__
屬性被賦值爲 null
,這時它的原型確實已經被修改成 null
,但想再經過對 __proto__
賦值的方式設置原型時是無效的,這時 __proto__
和一個普通屬性沒有區別,只能經過 Reflect.setPrototypeOf
或 Object.setPrototypeOf
才能修改原型。Reflect.setPrototypeOf
之因此能修改原型是由於它是直接修改對象的原型屬性,即內部直接對對象的 [[prototype]]
屬性賦值,而不會經過 __proto__
的 getter
const obj = { name: 'xiaoming' };
obj.__proto__ = null;
console.log(obj.__proto__); // => undefined
console.log(Reflect.getPrototypeOf(obj)); // => null
// 再次賦值爲 null
obj.__proto__ = null;
console.log(obj.__proto__); // => null
obj.__proto__ = { a: 1 };
console.log(obj.__proto__); // => { a: 1 }
// __proto__ 就像一個普通屬性同樣 obj.xxx = { a: 1 }
// 並無將原型設置成功
console.log(Reflect.getPrototypeOf(obj)); // => null
Reflect.setPrototypeOf(obj, { b: 2 });
// __proto__ 被設置爲 null 後,obj 的 __proto__ 屬性和一個普通的屬性沒有區別
console.log(obj.__proto__); // => { a: 1 }
// 使用 Reflect.setPrototypeOf 是能夠設置原型的
console.log(Reflect.getPrototypeOf(obj)); // => { b: 2 }
複製代碼
經過改變一個對象的 [[Prototype]]
屬性來改變和繼承屬性會對性能形成很是嚴重的影響且性能消耗的時間也不是簡單的花費在 obj.__proto__ = ...
語句上,它還會影響到全部繼承自該 [[Prototype]]
的對象,若關心性能就不該該修改一個對象的 [[Prototype]]
若要讀取或修改對象的 [[Prototype]]
屬性,建議使用以下方案,可是此時設置對象的 [[Prototype]] 依舊是一個緩慢的操做,若性能是一個考慮問題,就要避免這種操做
// 獲取
Object.getPrototypeOf();
Reflect.getPrototypeOf();
// 修改
Object.setPrototypeOf();
Reflect.setPrototypeOf();
複製代碼
__proto__
存在於全部的對象上,是對象所獨有的且指向它的原型對象。它的做用就是當你在訪問一個對象屬性時,若該對象內部不存在這個屬性,則會去它的 __proto__
屬性所指向的對象(原型對象,原型也是對象也有它本身的原型)上查找,若原型對象依舊不存在這個屬性,則去其原型對象的 __proto__
屬性所指向的原型對象上去查找...以此類推,直到找到 null
,返回 undefined,這個查找的過程也就構成了咱們常說的 原型鏈
由於在 JS 中是沒有類的概念的,爲了實現相似繼承的方式,經過
__proto__
將對象和原型聯繫起來組成原型鏈
,得以讓對象能夠訪問到不屬於本身的屬性
以前說對象的建立方式主要有兩種,一種是 new
操做符後跟函數調用,另外一種是字面量表示法
第三種就是 ES5 提供的 Object.create()
方法,該方法會建立一個新對象,第一個參數接收一個對象,將會做爲與新建立對象關聯的原型對象,第二個可選參數是屬性描述符(不經常使用,默認是 undefined
)
日常所看到的空對象其實並非嚴格意義上的空對象,它的原型對象指向Object.prototype
,還能夠繼承 hasOwnProperty
、toString
、valueOf
等方法
若要建立一個新對象同時繼承另外一個對象的 [[Prototype]]
,推薦使用 Object.create()
若想生成一個不繼承任何屬性的對象,可以使用 Object.create(null)
若想生成一個日常字面量方法生成的對象,須要將其原型對象指向Object.prototype
let obj = Object.create(Object.prototype);
// 等價於
let obj = {};
複製代碼
const obj= Object.create(Object.prototype);
obj.__proto__ === Object.prototype; // true
const obj = Object.create(null);
obj.__proto__ === Object.prototype; // false;
console.log(obj.__proto__); // undefined
複製代碼
簡易模擬 Object.create
function createObj(proto) {
const F = function() {};
F.prototype = proto;
return new F();
}
複製代碼
當在一個對象 obj
上訪問某個屬性時,若該屬性不存在於 obj
上,則會經過 __proto__
去對象的原型即 obj.__proto__
上去找這個屬性,如有則返回該屬性,沒有則繼續去對象 obj 的原型的原型即 obj.__proto__.__proto__
去找 ... 重複以上步驟,一直訪問到 純對象的原型
即 Object.prototype
,沒有的話繼續往上找即 Object.prototype.__proto__
,即 null
,此時直接返回 undefined
console.log(new Object().__proto__.__proto__); // null
Object.prototype.__proto__ === null; // null
複製代碼
這就推出了原型鏈之因此叫原型鏈而不叫原型環,說明它是善始善終的,原型鏈的頂層就是 null
,返回 undefined
,因此原型鏈不會無限的找下去
所以原型鏈
能夠描述爲由對象的 __proto__
屬性將對象和原型聯繫起來直到Object.prototype.__proto__
爲null
的鏈就是原型鏈
function Student(name, grade) {
this.name = name;
this.grade = grade;
}
const stu = new Student();
console.log(stu.gender); // => undefined
複製代碼
訪問 stu.gender 的整個過程以下圖:
函數 Student
的原型鏈應該是這樣的
上文介紹了 prototype
和 __proto__
的區別,其中原型對象 prototype
是構造函數的屬性,__proto__
是每一個實例對象上都有的屬性,這兩個並不同,但指向同個對象,如上面例子 stu.__proto__
和 Student.prototype
指向同個對象
那原型鏈的構建是依賴於 prototype
仍是 __proto__
呢?
上圖中,Student.prototype
中的 prototype
並無構建成一條原型鏈,其只是指向原型鏈中的某一處。原型鏈的構建依賴於 __proto__
,如上圖經過 stu.__proto__
指向 Student.prototype
,stu.__proto__.__proto__
指向 Object.prototyp
,如此一層一層最終連接到 null
能夠這麼理解,Student 是一個 constructor 也是一個 function,它身上有着 prototype 的 reference,只要調用 stu = new Student() 就會將
stu.__proto__
指向到 Student 的 prototype 對象
不要使用相似 Bar.prototype = Foo
,由於這不會執行 Foo
的原型,而是指向函數 Foo
。所以原型鏈將會回溯到 Function.prototype
而不是 Foo.prototype
,所以 Foo 原型上的方法將不會在 Bar 的原型鏈上
function Foo() {
return 'foo';
}
Foo.prototype.getMethod = function() {
return 'method';
}
function Bar() {
return 'bar';
}
Bar.prototype = Foo; // Bar.prototype 指向到函數 Foo
const bar = new Bar();
console.dir(bar);
bar.method(); // bar.getMethod is not a function
bar.__proto__ === Foo.prototype; // false
複製代碼
經過一個對象改變了原型上的引用值類型的屬性,則全部對象實例的這個屬性值都會隨之更改
依據當自身沒有這個屬性時就會向上往原型查詢的說法,再次刪除這個屬性是否是就能夠刪除原型上的屬性了?然而事實並無,因而可知對象實例並不能刪除原型上的屬性
誰調用這個方法,這個方法中的
this
就指向這個調用它的對象
日常判斷一個變量的類型常常會使用 typeof
運算符,但對於引用類型來講並不能很好區分(除了函數對象會返回 function
外其餘都返回 object
)
來看一下 MDN
上對於 instanceof
運算符的描述
instanceof 運算符用於測試構造函數的 prototype 屬性是否出如今對象實例的原型鏈中的任何位置
instanceof
和 typeof
很是的相似。instanceof
用於判斷對象是不是某個構造函數的實例,若 obj instanceof A
,就說明 obj
是 A
的實例 f
它的原理一句話歸納就是:
obj instanceof 構造器 A
等同於判斷 A 的 prototype 是否是 obj 的原型
instanceof
操做符左邊是一個對象,右邊是一個構造函數,在左邊對象的原型鏈上查找(經過 __proto__
)直到找到右邊構造函數的 prototype
屬性就返回 true
,或查找到頂層 null
(Object.prototype.__proto__
),就返回 false
// 定義構造函數
function C(){}
function D(){}
const o1 = new C();
o1 instanceof C; // true,由於 Object.getPrototypeOf(o1) === C.prototype
o1 instanceof D; // false,由於 D.prototype 不在 o1 的原型鏈上
o1 instanceof Object; // true,由於 Object.prototype.isPrototypeOf(o1) 返回 true
C.prototype instanceof Object; // true,同上
C.prototype = {};
const o2 = new C();
o2 instanceof C; // true
o1 instanceof C; // false,C.prototype 指向了一個空對象,這個空對象不在 o1 的原型鏈上
D.prototype = new C(); // 繼承
const o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 由於 C.prototype 如今在 o3 的原型鏈上
複製代碼
與 typeof
方法不一樣的是,instanceof
方法要求開發者明確地確認對象爲某特定類型
簡單模擬實現
// 第一種
// 參數 obj 表示 instanceof 左邊的對象
// 參數 Constructor 表示 instanceof 右邊的構造函數
function myInstanceOf(obj, Constructor) {
// 取構造函數顯示原型
let rightP = Constructor.prototype;
// 取對象隱式原型
let leftP = obj.__proto__;
// 到達原型鏈頂層還未找到則返回 false
if (leftP === null) {
return false;
}
// 對象實例的隱式原型等於構造函數顯示原型則返回 true
if (leftP === rightP) {
return true;
}
// 遞歸查找原型鏈上一層
return myInstanceOf(obj.__proto__, Constructor)
}
// 第二種
function myInstanceof(left, right) {
let prototype = right.prototype;
left = left.__proto__;
while (true) {
if (left === null) return false;
if (prototype === left) return true;
left = left.__proto__;
}
}
複製代碼
如今就能夠解釋一些比較使人費解的結果了
let fn = function() {};
let arr = [];
fn instanceof Function; // true
fn instanceof Object; // true
// 1. fn.__proto__ === Function.prototype;
// 2. fn.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;
arr instanceof Array; // true
arr instanceof Object; // true
// 1. arr.__proto__ === Array.prototype;
// 2. arr.__proto__.__proto__ === Array.prototype.__proto__ === Object.prototype;
Object instanceof Object; // true
// 1. Object.__proto__ === Function.prototype;
// 2. Object.__proto__.__proto__ === Function.prototype.__proto__ === Object.prototype;
Function instanceof Function; // true
Function instanceof Object; // true
// 1. Function.__proto__ === Function.prototype;
// 2. Function.__proto__.__proto__ === Object.prototype;
Foo instanceof Function; // true
Foo instanceof Foo; // false
// 1. Foo.__proto__ === Function.prototype;
// 2. Foo.__proto__.__proto__ === Function.prototype.__proto__ = Object.prototype;
// 3. Foo.__proto__.__proto__.__proto__ === Object.prototype.__proto__ = null;
// 4. null
複製代碼
總結:instanceof
運算符用於檢查右邊構造函數的 prototype
屬性是否出如今左邊對象的原型鏈中的任何位置,其實它表示的是一種原型鏈繼承的關係
上面提到的
Object.__proto__ === Function.prototype
和Function.__proto__ === Function.prototype
究竟是爲何呢?
ECMAScript 上的定義
The value of the [[Prototype]] internal property of the Object prototype object is null, the value of the [[Class]] internal property is "Object", and the initial value of the [[Extensible]] internal property is true
Object.prototype
表示 Object
的原型對象,其 [[Prototype]]
屬性是 null
,訪問器屬性 __proto__
暴露了一個對象的內部 [[Prototype]]
。 Object.prototype
並非經過 Object
函數建立的,爲何呢?看以下代碼
function Foo() {
this.value = 'foo';
}
let foo = new Foo();
foo.__proto__ === Foo.prototype; // true
複製代碼
實例對象的 __proto__
指向構造函數的 prototype
,即 foo.__proto__
指向 Foo.prototype
,但 Object.prototype.__proto__
是 null
,因此 Object.prototype
並非經過 Object
函數建立的,那它如何生成的?其實 Object.prototype
是引擎根據 ECMAScript
規範創造的一個對象
因此能夠說:全部實例都是對象,可是對象不必定都是實例
不考慮 null
的狀況下,Object.prototype
就是原型鏈的頂端,全部對象實例均可以繼承它的 toString
等方法和屬性
ECMAScript 上的定義
The Function prototype object is itself a Function object (its [[Class]] is "Function").
The value of the [[Prototype]] internal property of the Function prototype object is the standard built-in Object prototype object.
The Function prototype object does not have a valueOf property of its own; however, it inherits the valueOf property from the Object prototype Object
Function.prototype
對象是一個函數(對象),其 [[Prototype]]
內部屬性值指向內建對象 Object.prototype
。Function.prototype
對象自身沒有 valueOf
屬性,其從 Object.prototype
對象繼承了 valueOf
屬性
Function.prototype
的 [[Class]]
屬性是 Function
,因此這是一個函數,但又不大同樣。爲何這麼說呢?由於只有函數纔有 prototype
屬性,但並非全部函數都有這個屬性,由於 Function.prototype
這個函數就沒有
Function.prototype
// ƒ () { [native code] }
Function.prototype.prototype
// undefined
複製代碼
下面這個函數也沒有 prototype
屬性
let fun = Function.prototype.bind();
// ƒ () { [native code] }
fun.prototype
// undefined
複製代碼
爲何沒有呢?個人理解是 Function.prototype
是引擎建立出來的函數,引擎認爲不須要給這個函數對象添加 prototype
屬性,否則 Function.prototype.prototype…
將無休無止而且沒有存在的意義
Function.prototype
不可寫、不可配置、不可遍歷,即它永遠指向固定的一個對象且是其餘全部函數的原型對象,全部函數自己的 __proto__
指向它
引擎首先建立了 Object.prototype
,而後建立了 Function.prototype
而且經過 __proto__
將二者聯繫了起來
JS 中 Obejct
和 Function
都是構造函數(構造函數也是函數),和 object
、function
不是一個東西,分別用於建立 對象
與 函數
實例
ECMAScript 上的定義
The value of the [[Prototype]] internal property of the Object constructor is the standard built-in Function prototype object
Object
做爲構造函數時,其 [[Prototype]]
內部屬性值指向 Function.prototype
,即
Object.__proto__ === Function.prototype; // true
複製代碼
Object
的全貌是:function Object() { ... }
,它是普通對象的構造函數,當 var foo = {}
時至關於實例化 Object
,即 new Object()
使用 new Object()
建立新對象時,這個新對象的 [[Prototype]]
內部屬性指向構造函數的 prototype
屬性,對應就是 Object.prototype
固然也能夠經過對象字面量等方式建立對象
[[Prototype]]
值是 Object.prototype
[[Prototype]]
值是 Array.prototype
function f(){}
函數建立的對象,其 [[Prototype]]
值是 Function.prototype
new fun()
建立的對象,其中 fun
是由 JavaScript
提供的內建構造器函數之一(Object
, Function
, Array
, Boolean
, Date
, Number
, String
等),其 [[Prototype]]
值是 fun.prototype
JavaScript
構造器函數建立的對象,其 [[Prototype]]
值就是該構造器函數的 prototype
屬性// 原型鏈:o.__proto__ -> Object.prototype -> null
let o = {a: 1};
// 原型鏈:a -> Array.prototype -> Object.prototype -> null
let a = ["yo", "whadup", "?"];
// 原型鏈:f -> Function.prototype -> Object.prototype -> null
function f(){
return 1;
}
// 原型鏈:fun -> Function.prototype -> Object.prototype -> null
let fun = new Function();
// 原型鏈:foo -> Foo.prototype -> Object.prototype -> null
function Foo() {}
let foo = new Foo();
// 原型鏈:foo -> Object.prototype -> null
function Foo() {
return {};
}
let foo = new Foo();
複製代碼
ECMAScript 上的定義
The Function constructor is itself a Function object and its [[Class]] is "Function". The value of the [[Prototype]] internal property of the Function constructor is the standard built-in Function prototype object
Function
構造函數是一個函數對象,其 [[Class]]
屬性是 Function
。Function
的 [[Prototype]]
屬性指向了 Function.prototype
,即
Function.__proto__ === Function.prototype; // true
複製代碼
Function
的全貌是:function Function() { ... }
,它是函數對象的構造函數,當 function foo() {}
時至關於實例化 Function
,即 new Function()
咱們知道函數的本質是經過 new Function()
生成的,但Function.prototype
是引擎本身建立的,因此又能夠得出一個結論
不是全部函數都是
new Function()
產生的
先看下面代碼
Object instanceof Function; // true
Object.__proto__ === Function.prototype; // true
Function instanceof Object; // true
Function.__proto__.__proto__ === Object.prototype; // true
Object instanceof Object; // true
Object.__proto__.__proto__ === Object.prototype; // true
Function instanceof Function; // true
Function.__proto__ === Function.prototype; // true
複製代碼
Object
構造函數繼承了 Function.prototype
,一切函數對象
都直接繼承自 Function
對象(系統內置的構造函數),函數對象
包括了 Function
、Object
、Array
、String
、Number
、RegExp
、Date
等,Function
其實不只用於構造函數,它也充當了 函數對象
的構造器
同時 Function
構造函數繼承了 Object.prototype
,這裏就產生了 雞和蛋
的問題。由於 Function.prototype
和 Function.__proto__
都指向 Function.prototype
對於 Function.__proto__ === Function.prototype
這一現象有 2 種解釋,爭論點在於 Function
對象是否是由 Function
構造函數建立的一個實例?
YES
:按照 JavaScript
中實例的定義,a
是 b
的實例即 a instanceof b
爲 true
,默認判斷條件就是 b.prototype
在 a
的原型鏈上。而 Function instanceof Function
爲 true
,本質上即 Object.getPrototypeOf(Function) === Function.prototype
,正符合此定義NO
:Function
是 built-in
的對象,即並不存在 Function 對象由 Function 構造函數建立
這樣顯然會形成雞生蛋蛋生雞的問題。實際上當直接寫一個函數時(如 function f() {}
或 x => x
),也不存在調用 Function
構造器,只有在顯式調用 Function
構造器時(如 new Function('x', 'return x')
)纔有我的偏向於第二種解釋,即先有 Function.prototype
而後有 function Function()
,因此就不存在雞生蛋蛋生雞問題了,把 Function.__proto__
指向 Function.prototype
,我的的理解是:其餘全部的構造函數均可以經過原型鏈找到 Function.prototype
,且 Function
本質也是一個函數對象,事實上 Function 只是一個祖先、一個構造函數,並非一個實例出來的函數對象,因此原本不必擁有 __proto__
這個屬性,但這樣的話會顯得 Function
很另類,因而也給它加上屬性 __proto__
並指向 Function.prototype
,只是爲了代表 Function
做爲一個原生構造函數,自己也是一個函數對象,並且這也保證了原型鏈的完整,讓 Function
能夠獲取定義在 Object.prototype
上的方法
一切函數對象(包括 Object 對象) 都直接繼承自 Function 對象,Function 對象直接繼承本身,最終繼承自 Object 對象,Object 和 Function 是互相繼承的關係
有了
Function.prototype
之後纔有了function Function()
,而後其餘的構造函數都是function Function()
生成的
一切對象都繼承自 Object.prototype
,而一切函數對象
都繼承自 Function.prototype
(Function.prototype
最終繼承自 Object.prototype
),即普通對象和函數對象的區別是:普通對象直接繼承了 Object.prototype
,而函數對象在中間還繼承了 Function.prototype
所以能夠得出如下總結:
Object
是全部對象的爸爸
,全部對象均可以經過__proto__
找到它Function
是全部函數的爸爸,全部函數均可以經過__proto__
找到它- 全部經過字面量表示法建立的普通對象的構造函數爲
Object
- 全部原型對象都是普通對象,構造函數爲
Object
- 全部函數的構造函數是
Function
Function.prototype
和Object.prototype
沒有原型對象Function.prototype
和Object.prototype
是兩個由引擎建立出來的特殊對象,除了這兩個特殊對象,其餘對象都是經過構造器new
出來的- 函數的
prototype
是一個對象,即原型。對象的__proto__
指向原型,__proto__
將對象和原型鏈接起來組成了原型鏈Function.prototype.__proto__ === Object.prototype
Object.__proto__ === Function.prototype
Function.__proto__ === Function.prototype
Function.__proto__ === Object.__proto__
Object.prototype.__proto__ === null
Object => Function.prototype => Object.prototype => null
Function => Function.prototype => Object.prototype => null
如果自定義的構造函數,造成的原型鏈以下:Foo => Function.prototype => Object.prototype => null
經過自定義構造函數實例化的對象,造成的原型鏈以下:obj => Foo.prototype => Object.prototype => null
JavaScript 內置類型是瀏覽器內核自帶的,瀏覽器底層對 JavaScript 的實現基於 C/C++,那麼瀏覽器在初始化 JavaScript 環境時都發生了什麼?
至此全部內置類型構建完成
曾經 lodash 爆出了一個嚴重的安全漏洞:Lodash 庫爆出嚴重安全漏洞,波及 400 萬+項目,這個安全漏洞就是因爲原型污染致使的,Lodash 庫中的函數「defaultsDeep」頗有可能會被欺騙添加或修改 Object.prototype 的屬性,最終可能致使 Web 應用程序崩潰或改變其行爲,具體取決於受影響的用例
雖說任何一個原型被污染了都有可能致使問題,但通常提原型污染說的就是 Object.prototype
被污染
'use strict';
const express = require('express');
const bodyParser = require('body-parser');
const cookieParser = require('cookie-parser');
const path = require('path');
const isObject = (obj) => obj && obj.constructor && obj.constructor === Object;
function merge(a, b) {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a;
}
function clone(a) {
return merge({}, a);
}
// Constants
const PORT = 8080;
const HOST = '127.0.0.1';
const admin = {};
// App
const app = express();
app.use(bodyParser.json());
app.use(cookieParser());
app.use('/', express.static(path.join(__dirname, 'views')));
app.post('/signup', (req, res) => {
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body);
if (copybody.name) {
res.cookie('name', copybody.name).json({
done: 'cookie set',
});
} else {
res.json({
error: 'cookie not set',
});
}
});
app.get('/getFlag', (req, res) => {
var аdmin = JSON.parse(JSON.stringify(req.cookies));
if (admin.аdmin == 1) {
res.send('hackim19{}');
} else {
res.send('You are not authorized');
}
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
複製代碼
這段代碼的漏洞就在於 merge 函數上,能夠這樣攻擊:
curl -vv --header 'Content-type: application/json' -d '{"__proto__": {"admin": 1}}' 'http://127.0.0.1:4000/signup';
curl -vv 'http://127.0.0.1/getFlag'
複製代碼
首先請求 /signup
接口,在 NodeJS
服務中,咱們調用了有漏洞的 merge
方法,並經過 __proto__
爲 Object.prototype
(由於 {}.__proto__ === Object.prototype
) 添加上一個新的屬性 admin
且值爲 1
再次請求 getFlag
接口訪問了 Object
原型上的 admin
,條件語句 admin.аdmin == 1
爲 true
,服務被攻擊
其實原型污染大多發生在調用會修改或擴展對象屬性的函數時,如 lodash 的 defaults、jQuery 的 extend,預防原型污染最主要仍是要有防患意識,養成良好的編碼習慣
Object.create(null)
Object.create(null)
建立沒有原型的對象,即使對它設置 __proto__
也沒有用,由於它的原型一開始就是 null
,沒有 __proro__
的 setter
Object.freeze(obj)
能夠經過 Object.freeze(obj)
凍結對象 obj,被凍結的對象不能被修改屬性,成爲不可擴展對象。不能修改不可擴展對象的原型,不然會拋 TypeError:
const obj = Object.freeze({ name: 'xiaoHong' });
obj.xxx = 666;
console.log(obj); // => { name: 'xiaoHong' }
console.log(Object.isExtensible(obj)); // => false
obj.__proto__ = null; // => TypeError: #<Object> is not extensible
複製代碼
關於原型污染可閱讀:最新:Lodash 嚴重安全漏洞背後你不得不知道的 JavaScript 知識
原型存在的意義就是組成原型鏈:引用類型皆對象,每一個對象都有原型,原型也是對象,也有它本身的原型,一層一層的組成了原型鏈
原型鏈存在的意義就是繼承:訪問對象屬性時,在對象自己找不到,就在原型鏈上一層一層往上找,說白了就是一個對象能夠訪問其餘對象的屬性
繼承存在的意義就是屬性共享:好處一是代碼重用(字面意思);好處二是可擴展,不一樣對象可能繼承相同的屬性,也能夠定義只屬於本身的屬性
將父類的實例做爲子類的原型
function Parent() {
this.name = 'tn';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Son () {};
// 關鍵,建立 Parent 的實例並將該實例賦值給 Son.prototype
Son.prototype = new Parent();
const son1 = new Son();
console.log(son1.getName()); // tn
// 缺點
function Parent () {
this.names = ['licy', 'tn'];
}
function Son () {}
Son.prototype = new Parent();
const son1 = new Son();
son1.names.push('yayu');
console.log(son1.names); // ["licy", "tn", "yayu"]
const son2 = new Son();
console.log(son2.names); // ["licy", "tn", "yayu"]
複製代碼
prototype
對父類進行實例化實現的,所以在構建子類實例時是沒法向父類傳遞參數的,於是在實例化父類時也沒法對父類構造函數內的屬性進行初始化使用父類的構造函數來加強子類實例,利用 call
和 apply
可改變 this
指向的特色,將父類構造函數內容複製給子類構造函數,因爲父類中給 this
綁定屬性,所以子類天然也就繼承父類的共有屬性,這是全部繼承中惟一不涉及到 prototype
的繼承
function Parent (name) {
this.books = ['js','css'];
this.name = name;
}
Parent.prototype.showBooks = function() {
console.log(this.books);
}
function Son (name) {
Parent.call(this, name);
}
const son1 = new Son('tn');
console.log(son1.name); // tn
son1.showBooks(); // TypeError: son1.showBooks is not a function
const son2 = new Son('licy');
console.log(son2.name); // licy
function SuperType(){
this.color=["red","green","blue"];
}
function SubType(){
//繼承自 SuperType
SuperType.call(this);
}
const instance1 = new SubType();
instance1.color.push("black");
console.log(instance1.color); //["red", "green", "blue", "black"]
const instance2 = new SubType();
console.log(instance2.color); //["red", "green", "blue"]
複製代碼
原型鏈繼承
和借用構造函數繼承
結合
用原型鏈實現對原型屬性和方法的繼承,用借用構造函數技術來實現實例屬性的繼承
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){ console.log(this.name); };
function SubType(name, age){
// 繼承屬性
// 第二次調用 SuperType()
// 第二次又給子類的構造函數添加了父類的 name, colors 屬性
// 使用子類建立的實例對象上的同名屬性覆蓋了子類原型中的同名屬性,這形成了性能浪費
SuperType.call(this, name);
this.age = age;
}
// 繼承方法,構建原型鏈
// 第一次調用 SuperType()
// 第一次給子類的原型添加了父類的 name, colors 屬性
SubType.prototype = new SuperType();
// 重寫 SubType.prototype 的 constructor 屬性,指向本身的構造函數 SubType
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){ alert(this.age); };
const instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors); //["red", "blue", "green", "black"]
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
const instance2 = new SubType("Greg", 27);
console.log(instance2.colors); //["red", "blue", "green"]
instance2.sayName(); //"Greg";
instance2.sayAge(); //27
複製代碼
ES5 Object.create
的模擬實現,利用一個空對象做爲中介,將傳入的對象做爲該空對象構造函數的原型,object()
對傳入的對象執行了一次淺複製
function object(obj){
// 聲明一個過渡對象
function F(){};
// 過渡對象的原型繼承傳入的對象
F.prototype = obj;
// 返回過渡對象的實例
return new F();
}
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
const anotherPerson = object(person);
const yetAnotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(anotherPerson.name) // "Greg"
console.log(yetAnotherPerson.name) // "Nicholas"
console.log(yetAnotherPerson.friends) //["Shelby", "Court", "Van", "Rob"]
複製代碼
Object.create()
的方法可以代替上面的 object
方法,Object.create()
方法規範化了原型式繼承function createAnother(original){
const clone = object(original); // 經過調用 object() 函數建立一個新對象
// 或
const clone = Object.create(o);
clone.sayHi = function(){
// 以某種方式來加強對象
alert("hi");
}
return clone; // 返回這個對象
}
const person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
複製代碼
寄生組合式繼承是寄生式繼承和借用構造函數繼承的組合,只調用了一次父類構造函數,解決了組合繼承有會兩次調用父類的構造函數形成浪費的缺點
function object(o) {
//聲明一個過渡對象
function F() {}
//過渡對象的原型繼承父對象
F.prototype = o;
//返回過渡對象的實例,該對象的原型繼承了父對象
return new F();
}
function prototype(child, parent) {
// 複製一份父類的原型副本到變量中
const prototype = object(parent.prototype);
// 加強對象,修正由於重寫子類的原型致使子類的 `constructor` 屬性被修改
prototype.constructor = child;
// 設置子類原型
child.prototype = prototype;
}
// 使用時
prototype(Child, Parent);
複製代碼
引用《JavaScript高級程序設計》中對寄生組合式繼承的誇讚就是:
這種方式的高效率體現它只調用了一次 Parent
構造函數而且所以避免了在 Parent.prototype
上面建立沒必要要的、多餘的屬性。與此同時原型鏈還能保持不變,所以可以正常使用 instanceof
和 isPrototypeOf
。開發人員廣泛認爲寄生組合式繼承是引用類型最理想的繼承範式,也是如今不少庫實現的方法
封裝
function inherit (Target, Origin) {
// 聲明一個過渡對象
function F () {};
// 過渡對象的原型繼承父對象,建立了父類原型的淺複製
F.prototype = Origin.prototype;
// 返回過渡對象的實例,該對象的原型繼承了父對象
Target.prototype = new F();
// 修正子類原型的構造函數
Target.prototype.constructor = Target;
// 沒法知道本身真正繼承至誰(記住最好,也不強求)
// 爲了保存一下它的父類,也用一個 uber 來記錄一下父類
// 由於 super 是保留字不能使用,因此使用了 uber
Target.prototype.uber = Origin.prototype;
}
複製代碼
雅虎的高端寫法,採用閉包的私有化變量
var inherit = (function () {
var F = function () {};
return function (Target, Origin) {
F.prototype = Origin.prototype;
Target.prototype = new F();
Target.prototype.constructor = Target;
Target.prototype.uber = Origin.prototype;
}
}());
複製代碼
Object.assign
會把 OtherSuperClass
原型上的方法屬性拷貝到 MyClass
原型上,使 MyClass
的全部實例均可使用 OtherSuperClass
上的方法
function MyClass() {
SuperClass.call(this);
OtherSuperClass.call(this);
}
// 繼承一個類
MyClass.prototype = Object.create(SuperClass.prototype);
// 混合其它
Object.assign(MyClass.prototype, OtherSuperClass.prototype);
// 從新指定constructor
MyClass.prototype.constructor = MyClass;
MyClass.prototype.myMethod = function() {
// do something
};
複製代碼
ES6 的繼承和寄生組合繼承類似,本質上 ES6 繼承是 ES5 繼承的一種語法糖extends
關鍵字主要用於類聲明或類表達式中,以建立一個類表示該類是另外某個類的子類
constructor
表示構造函數,一個類中只能有一個構造函數,有多個會報出SyntaxError
錯誤,若沒有顯式指定構造方法,則會添加默認的 constructor
方法,例子以下
class Rectangle {
// constructor
constructor(height, width) {
this.height = height; this.width = width;
}
// Getter
get area() { return this.calcArea() }
// Method
calcArea() { return this.height * this.width; }
}
const rectangle = new Rectangle(10, 20);
console.log(rectangle.area); // 輸出 200
// 繼承
class Square extends Rectangle {
constructor(length) {
super(length, length); // 若是子類中存在構造函數,則須要在使用「this」以前首先調用 super()。
this.name = 'Square';
}
get area() { return this.height * this.width; }
}
const square = new Square(10);
console.log(square.area); // 輸出 100
複製代碼
extends
繼承的核心代碼以下,其實現和上述的寄生組合式繼承方式相似
// extends 繼承的核心代碼以下,其實現和上述的寄生組合式繼承方式類似
function _inherits(subType, superType) {
// 建立對象,建立父類原型的一個副本
// 加強對象,彌補因重寫原型而失去的默認的constructor 屬性
// 指定對象,將新建立的對象賦值給子類的原型
subType.prototype = Object.create(superType && superType.prototype, {
constructor: {
value: subType,
enumerable: false,
writable: true,
configurable: true
}
});
if (superType) {
Object.setPrototypeOf ?
Object.setPrototypeOf(subType, superType) :
subType.__proto__ = superType;
}
}
複製代碼
總結
ReferenceError
let p = new Rectangle();
// ReferenceError
class Rectangle {}
複製代碼
Class extends
是 ES5 繼承的語法糖ES5
的繼承實質上是先建立子類的實例對象,而後再將父類的方法添加到 this
上
Child.prototype = new Parent() || Parent.apply(this) || Parent.call(this)
複製代碼
ES6
的繼承有所不一樣,在 ES6 class
中,實質上是先建立父類的實例對象 this
,而後再用子類的構造函數修改 this
。子類必須在 constructor
方法中調用 super
方法,不然新建實例時會報錯。這是由於子類沒有本身的 this
對象而是繼承父類的 this
對象,而後對其進行加工
function Page() {
return this.hosts;
}
Page.hosts = ['h1'];
Page.prototype.hosts = ['h2'];
const p1 = new Page();
const p2 = Page();
console.log(p1.hosts); // undefined
console.log(p2.hosts); // Uncaught TypeError: Cannot read property 'hosts' of undefined
複製代碼
緣由分析
new
時若 return
了對象,則會直接拿這個對象做爲 new
的結果,所以 p1
應該是 this.hosts
的結果,而在 new Page()
時,this
是一個以 Page.prototype
爲原型的 target
對象,因此這裏 this.hosts
能夠訪問到 Page.prototype.hosts
即 ['h2']
。所以 p1
就是等於 ['h2']
,['h2']
沒有 hosts
屬性因此返回 undefined
console.log(p2.hosts)
會報錯是由於 p2
是直接調用 Page
構造函數,這個時候 this
指向全局對象,全局對象並沒 hosts
屬性,所以返回 undefined
,往 undefined
上訪問 hosts
固然報錯