類與原型
通過原型這種機制,JavaScript 中的對象從其他對象繼承功能特性;這種繼承機制與經典的面向對象編程語言的繼承機制不同.
JavaScript 常被描述為一種基於原型的語言 (prototype-based language)——每個對象擁有一個原型對象,對象以其原型為模板、從原型繼承方法和屬性.原型對象也可能擁有原型,並從中繼承方法和屬性,一層一層、以此類推.這種關系常被稱為原型鏈 (prototype chain),它解釋了為何一個對象會擁有定義在其他對象中的屬性和方法.
准確地說,這些屬性和方法定義在 Object 的構造器函數(constructor functions)之上的 prototype 屬性上,而非對象實例本身.
在傳統的 OOP 中,首先定義“類”,此后創建對象實例時,類中定義的所有屬性和方法都被復制到實例中.在 JavaScript 中並不如此復制——而是在對象實例和它的構造器之間建立一個鏈接(它是proto屬性,是從構造函數的 prototype 屬性派生的),之后通過上溯原型鏈,在構造器中找到這些屬性和方法.
理解對象的原型(可以通過 Object.getPrototypeOf(obj)或者已被棄用的proto屬性獲得)與構造函數的 prototype 屬性之間的區別是很重要的.前者是每個實例上都有的屬性,后者是構造函數的屬性.也就是說,Object.getPrototypeOf(new Foobar())和 Foobar.prototype 指向着同一個對象.
函數可以有屬性, 每個函數都有一個特殊的屬性叫作原型(prototype).
測試一下
function pro(){}
pro.inner = "pro-inner"
pro.prototype.out = "out function"
console.log(pro)
var pro2 = function(){}
pro2.inner = "pro2-inner"
pro2.prototype.out = "var varible"
console.log(pro2.prototype)
構造函數普遍使用首字母大寫的命名方式,這不是 js 這個語言強制規定的,而是人們在使用的過程中一種約定成俗
控制台打印出來的對象
out: "var varible"
constructor: ƒ ()
inner: "pro2-inner"
length: 0
name: "pro2"
arguments: null
caller: null
prototype: {out: "var varible", constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: pen.js:7
[[Scopes]]: Scopes[1]
__proto__:
constructor: ƒ Object()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
hasOwnProperty: ƒ hasOwnProperty()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toString: ƒ toString()
valueOf: ƒ valueOf()
toLocaleString: ƒ toLocaleString()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
使用 new 運算符來在現在的這個原型基礎之上,創建一個 proIns 實例.使用 new 運算符的方法就是在正常調用函數時,在函數名的前面加上一個 new 前綴. 通過這種方法,在調用函數前加一個 new ,它就會返回一個這個函數的實例化對象.
var pro2 = function(){}
pro2.inner = "pro2-inner"
pro2.prototype.out = "var varible"
var proIns = new pro2()
proIns.prop = "add prop"
console.log(proIns)
console.log(proIns.prototype)
console.log(pro2.prototype)
控制台輸出的內容
prop: "add prop"
__proto__:
out: "var varible"
constructor: ƒ ()
__proto__: Object
proIns 的 proto 屬性就是 pro2.prototype,而 pro2.prototype 的proto就是 Object,當需要訪問 proIns 的某個屬性或者方法的時候,瀏覽器就會沿着原型鏈一直向上進行查找是否有該屬性/方法.
執行路徑:
var Pro = function(){}
Pro.prototype.inner="i am pro"
var proIns = new Pro()
console.log(proIns.__proto__)
console.log(proIns.__proto__.__proto__)
console.log(proIns.__proto__.__proto__.__proto__)
- 瀏覽器首先查找 proIns 自身是否有這個屬性
- 如果 proIns 沒有這個屬性, 然后瀏覽器就會在 proIns 的
__proto__
也就是中查找這個屬性(Pro.prototype) - 如果 proIns 的
__proto__
沒有這個屬性, 瀏覽器就會去查找 proIns 的__proto__
的__proto__
的屬性(Object.prototype) - 最終查找
proIns.__proto__.__proto__.__proto__
為空,瀏覽器判斷原型鏈上不存在該屬性,該屬性獲取 undefined
測試案例
定義一個構造器函數 Person
function Person(name, age, work) {
this.name = name;
this.age = age;
this.work = work;
this.study = function() {
console.log(this.name + "學習了" + this.work);
};
}
Person.prototype.eat = function() {
console.log(this.name + "依舊需要吃飯");
};
var programmer = new Person("稱序員", 42, "coding");
programmer.study();
programmer.eat();
console.log(programmer.toString())
此時輸出 programer 在控制台上
Person {name: "稱序員", age: 42, work: "coding", study: ƒ}
name: "稱序員"
age: 42
work: "coding"
study: ƒ ()
__proto__:
eat: ƒ ()
constructor: ƒ Person(name, age, work)
__proto__:
constructor: ƒ Object()
__defineGetter__: ƒ __defineGetter__()
__defineSetter__: ƒ __defineSetter__()
hasOwnProperty: ƒ hasOwnProperty()
__lookupGetter__: ƒ __lookupGetter__()
__lookupSetter__: ƒ __lookupSetter__()
isPrototypeOf: ƒ isPrototypeOf()
propertyIsEnumerable: ƒ propertyIsEnumerable()
toString: ƒ toString()
valueOf: ƒ valueOf()
toLocaleString: ƒ toLocaleString()
get __proto__: ƒ __proto__()
set __proto__: ƒ __proto__()
該案例總共調用了三個方法.過程如下:
- 首先執行 study,programmer 具有該方法,就直接調用執行,輸出內容
- 然后向下執行,執行 eat 方法,瀏覽器查找 programmer 上是否擁有該方法,瀏覽器沒有在 programmer 上查找到 eat 方法
- 瀏覽器檢查 programmer 的原型對象(即 Person 構造函數的 prototype 屬性所指向的對象)是否具有可用的 eat() 方法,瀏覽器找到並執行該方法
- 繼續向下執行,執行 toString 方法
- 瀏覽器會先查找 programmer 然后查找 Person 構造器的 prototype,都沒有查找到該方法
- 由於原型也是對象,構造屬性 constructor 指向 Object,所以此時瀏覽器會查找 Person() 構造函數的 prototype 屬性所指向的對象的原型對象(即 Object 構造函數的 prototype 屬性所指向的對象)是否具有可用的 toString() 方法.瀏覽器查找到 toString 方法,執行.
原型鏈中的方法和屬性沒有被復制到其他對象——它們被訪問需要通過前面所說的“原型鏈”的方式.上面的執行也是先查找 programmer,之后查找
programmer.__proto__
,最后查找programmer.__proto__.__proto__
沒有官方的方法用於直接訪問一個對象的原型對象=>原型鏈中的“連接”被定義在一個內部屬性中,在 JavaScript 語言標准中用 [[prototype]] 表示(參見 ECMAScript).然而,大多數現代瀏覽器還是提供了一個名為 __proto__
(前后各有 2 個下划線)的屬性,其包含了對象的原型.
prototype 屬性
programmer 繼承的屬性和方法是定義在 prototype 屬性之上的(你可以稱之為子命名空間 (sub namespace) )——那些以 Object.prototype. 開頭的屬性,而非僅僅以 Object. 開頭的屬性.prototype 屬性的值是一個對象,我們希望被原型鏈下游的對象繼承的屬性和方法,都被儲存在其中.
- 函數是一種對象,每一個函數都自動擁有 prototype 屬性
- prototype 屬性是一個對象,默認有一個不可枚舉的 constructor 屬性,指向函數本身
於是 Object.prototype.watch()、Object.prototype.valueOf() 等等成員,適用於任何繼承自 Object() 的對象類型,包括使用構造器創建的新的對象實例.
Object.is()、Object.keys(),以及其他不在 prototype 對象內的成員,不會被“對象實例”或“繼承自 Object() 的對象類型”所繼承.這些方法/屬性僅能被 Object() 構造器自身使用.
例如
const str = 'today is sunshine';
console.log(str.indexOf(1));
str 就相當於通過 new String()擁有了一些有用的方法
我們經常使用的var obj = {}
,通過 Object.create 表示:
var obj = {};
// 以字面量方式創建的空對象就相當於:
var obj = Object.create(Object.prototype);
定義在 prototype 上的方法,必須在實例調用之前進行聲明.
為什么需要 prototype
用構造函數創建每一個實例對象,有些屬性和方法都是一模一樣的內容,每一次生成一個實例,都必須為重復的內容.這樣會消耗更多內存,也缺乏效率.使用
prototype 讓共用屬性和方法在內存中只生成一次,然后所有實例都指向那個內存地址.
Javascript 規定,每一個構造函數都有一個 prototype 屬性,指向原型對象.這個對象的所有屬性和方法,都會被構造函數的實例繼承
函數和構造函數
Function 構造函數創建一個新的 Function 對象.直接調用此構造函數可用動態創建函數,但會遭遇來自 eval 的安全問題和相對較小的性能問題.然而,與 eval 不同的是,Function 構造函數只在全局作用域中運行.每個 JavaScript 函數實際上都是一個 Function 對象 => (function(){}).constructor === Function
- 每一個函數都有 prototype 屬性 => func.prototype
- func.prototype 默認有 constructor,func.prototype.constructor
- 構造函數指向函數本身,func.prototype.constructor===func
Object.create()
......
var singer = Object.create(programmer)
......
console.log(programmer)
console.log(singer.__proto__)
console.log(singer.__proto__ === programmer)
singer.__proto__ === programmer
得到的結果為 true,實際Object.create()方法創建一個新對象,使用現有的對象來提供新創建的對象的proto.
constructor 屬性
每個實例對象都從原型中繼承了一個 constructor 屬性,該屬性指向了用於構造此實例對象的構造函數.
console.log(programmer.constructor)
console.log(singer.constructor)
測試都將返回 Person() 構造器,因為該構造器包含這些實例的原始定義.
構造器是一個函數,故可以通過圓括號調用;只需在前面添加 new 關鍵字,便能將此函數作為構造器使用.
var loser = new programmer.constructor("落魄者",108,"快餓死了")
loser.eat() // 落魄者依舊需要吃飯
原型鏈
原型鏈的經典圖:
每個實例對象( object )都有一個私有屬性(稱之為 __proto__
)指向它的構造函數的原型對象(prototype ).該原型對象也有一個自己的原型對象( __proto__
) ,層層向上直到一個對象的原型對象為 null.根據定義,null 沒有原型,並作為這個原型鏈中的最后一個環節.
幾乎所有 JavaScript 中的對象都是位於原型鏈頂端的 Object 的實例.
繼承
JavaScript 對象有一個指向一個原型對象的鏈.當試圖訪問一個對象的屬性時,它不僅僅在該對象上搜尋,還會搜尋該對象的原型,以及該對象的原型的原型,依次層層向上搜索,直到找到一個名字匹配的屬性或到達原型鏈的末尾.
遵循 ECMAScript 標准,someObject.[[Prototype]] 符號是用於指向 someObject 的原型.從 ECMAScript 6 開始,[[Prototype]] 可以通過 Object.getPrototypeOf() 和 Object.setPrototypeOf() 訪問器來訪問.這個等同於 JavaScript 的非標准但許多瀏覽器實現的屬性 __proto__
.
它不應該與構造函數 func 的 prototype 屬性相混淆.被構造函數創建的實例對象的 [[prototype]] 指向 func 的 prototype 屬性.Object.prototype 屬性表示 Object 的原型對象.
function Person(name, age, work) {
this.name = name;
this.age = age;
this.work = work;
this.study = function() {
console.log(this.name + "學習了" + this.work);
};
}
Person.prototype.name = "Person";
Person.prototype.type= "地球人";
Person.prototype.eat = function() {
console.log(this.name + "依舊需要吃飯");
};
var programmer = new Person("稱序員", 42, "coding");
programmer.study();
programmer.eat();
console.log(programmer)
console.log(programmer.__proto__)
console.log(programmer.constructor.prototype)
根據經典圖可以追溯 programmer 的原型
programmer.__proto__
指向 Person.prototypeprogrammer.__proto__.__proto__
指向 Object.prototypeprogrammer.__proto__.__proto__.__proto__
指向 null
屬性和方法的優先級
在實例上有一個屬性,在原型上也有屬性,優先執行近的,javascript 中的兩大鏈式 => 原型鏈和作用域鏈都是'就近原則'
function Person(name, age, work) {
this.name = name;
this.age = age;
this.work = work;
this.study = function() {
console.log(this.name + "學習了" + this.work);
};
}
var programmer = new Person("稱序員", 42, "coding");
programmer.eat = function(){
console.log("干掉了Person的eat方法")
}
programmer.type="外星人"
Person.prototype.name = "Person";
Person.prototype.type= "地球人";
Person.prototype.eat = function() {
console.log(this.name + "依舊需要吃飯");
};
programmer.eat() // 干掉了Person的eat方法
console.log(programmer.type) // 外星人
上面特別更改了賦值的順序,依舊是執行實例上的方法和屬性,這種情況被稱為"屬性遮蔽 (property shadowing)",java 語言中這就是方法重寫.
使用語法結構創建的對象
var o = {a: 1};
// o 這個對象繼承了 Object.prototype 上面的所有屬性
// o 自身沒有名為 hasOwnProperty 的屬性
// hasOwnProperty 是 Object.prototype 的屬性
// 因此 o 繼承了 Object.prototype 的 hasOwnProperty
// Object.prototype 的原型為 null
// 原型鏈如下:
// o ---> Object.prototype ---> null
var a = ["yo", "whadup", "?"];
// 數組都繼承於 Array.prototype
// (Array.prototype 中包含 indexOf, forEach 等方法)
// 原型鏈如下:
// a ---> Array.prototype ---> Object.prototype ---> null
function f(){
return 2;
}
// 函數都繼承於 Function.prototype
// (Function.prototype 中包含 call, bind等方法)
// 原型鏈如下:
// f ---> Function.prototype ---> Object.prototype ---> null
使用構造器創建的對象
在 JavaScript 中,構造器其實就是一個普通的函數.當使用 new 操作符 來作用這個函數時,它就可以被稱為構造方法(構造函數).
function Graph() {
this.vertices = [];
this.edges = [];
}
Graph.prototype = {
addVertex: function(v){
this.vertices.push(v);
}
};
var g = new Graph();
// g 是生成的對象,他的自身屬性有 'vertices' 和 'edges'.
// 在 g 被實例化時,g.[[Prototype]] 指向了 Graph.prototype.
使用 Object.create 創建的對象
ECMAScript 5 中引入了一個新方法:Object.create().可以調用這個方法來創建一個新對象.新對象的原型就是調用 create 方法時傳入的第一個參數:
var a = {a: 1};
// a ---> Object.prototype ---> null
var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (繼承而來)
var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null
var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); // undefined, 因為d沒有繼承Object.prototype
使用 class 關鍵字創建的對象
ECMAScript6 引入了一套新的關鍵字用來實現 class.使用基於類語言的開發人員會對這些結構感到熟悉,但它們是不同的.JavaScript 仍然基於原型.這些新的關鍵字包括 class, constructor,static,extends 和 super.
"use strict";
class Polygon {
constructor(height, width) {
this.height = height;
this.width = width;
}
}
class Square extends Polygon {
constructor(sideLength) {
super(sideLength, sideLength);
}
get area() {
return this.height * this.width;
}
set sideLength(newLength) {
this.height = newLength;
this.width = newLength;
}
}
var square = new Square(2);
查找原型鏈的性能
在原型鏈上查找屬性比較耗時,對性能有副作用,這在性能要求苛刻的情況下很重要.另外,試圖訪問不存在的屬性時會遍歷整個原型鏈.
遍歷對象的屬性時,原型鏈上的每個可枚舉屬性都會被枚舉出來.要檢查對象是否具有自己定義的屬性,而不是其原型鏈上的某個屬性,則必須使用所有對象從 Object.prototype 繼承的 hasOwnProperty 方法.
hasOwnProperty 和 Object.keys() 是 JavaScript 中處理屬性並且不會遍歷原型鏈的方法.
檢查屬性是否為 undefined 是不能夠檢查其是否存在的.該屬性可能已存在,但其值恰好被設置成了 undefined.
4 個用於拓展原型鏈的方法
- New-initialization
- Object.create
- Object.setPrototypeOf
- _proto__
prototype 和 Object.getPrototypeOf
prototype 是用於類的,而 Object.getPrototypeOf() 是用於實例的(instances),兩者功能一致.
對象
一切引用類型都是對象
console.log(typeof x); // undefined
console.log(typeof 10); // number
console.log(typeof 'abc'); // string
console.log(typeof true); // boolean
console.log(typeof function () {}); //function
console.log(typeof [1, 'a', true]); //object
console.log(typeof { a: 10, b: 20 }); //object
console.log(typeof null); //object
console.log(typeof new Number(10)); //object
- undefined、number、string、boolean 是值類型
- 函數、對象、數組、null
判斷值類型的用 typeof,判斷引用類型的用 instanceof
對象就是一些屬性集合
var obj = {
a:10,
b:function (){},
c:function (){}
}
對象里面一切都是屬性,方法也是屬性,以鍵值對的形式表現出來
函數定義屬性
var func = function () {
}
func.a = 10;
func.b = function () {
console.log('hello world');
}
func.c = {
name:'123',
year:1988
}
對象都是通過函數創建的
function Func(){
this.name = 'lili';
this.year = 1988;
}
var fn1 = new Func();
var obj = {a:20,b:30};
var arr = [1,2,3];
<!--等同於-->
var obj = new Object();
obj.a = 20;
obj.b = 30;
var arr = new Array();
arr[0] = 1;
arr[1] = 2;
arr[2] = 3;
對象是函數創建的,函數是一種對象
函數和對象的關系
函數就是對象的一種
var func = function (){};
console.log(func instanceof Object); // true
對象都是通過函數進行創建的
//var obj = { a: 10, b: 20 };
//var arr = [5, 'x', true];
<!--以上代碼的本質-->
var obj = new Object();
obj.a = 10;
obj.b = 20;
var arr = new Array();
arr[0] = 5;
arr[1] = 'x';
arr[2] = true;
isPrototypeOf 判斷一個對象象是否為一個實例的原型
console.log(a.prototype.isPrototypeOf(b));
console.log(b.prototype.isPrototypeOf(b));
propertyIsEnumerable 方法返回一個布爾值,表明指定的屬性名是否是當前對象可枚舉的自身屬性
for(var key in obj) {
f(obj.propertyIsEnumerable(key) {
<!--do somethings-->
};
};
- 判斷給定的屬性是否可以用 for...in 語句進行枚舉同時也是對象的自有屬性.
- for ... in 枚舉是包含原型鏈上的屬性的,propertyIsEnumerable 作用於原型方法上時,始終是返回 false 的
- for...in 可以枚舉對象本身的屬性和原型上的屬性,而 propertyIsEnumerable 只能判斷本身的屬性是否可以枚舉
- 預定義的屬性不是可列舉的,而用戶定義的屬性總是可列舉的.所以如果你只想遍歷對象本身的屬性
原型鏈
- 訪問一個對象的屬性時,先在基本屬性中查找,如果沒有,再沿着proto這條鏈向上找,這就是原型鏈.
- 當我們用 obj.xxx 訪問一個對象的屬性時,JavaScript 引擎先在當前對象上查找該屬性,如果沒有找到,就到其原型對象上找,如果還沒有找到,就一直上溯到 Object.prototype 對象,最后,如果還沒有找到,就只能返回 undefined.
隱式原型proto
每個對象都已一個proto,指向創建這個對象的函數的 prototype
function Phone() {
}
var mi = new Phone();
console.log(mi instanceof Object); //true
console.log(mi instanceof Array); //false
console.log(mi instanceof Function); //false
console.log(mi instanceof Phone); //true
// 實例對象的隱式原型鏈
console.log('實例對象指向Phone.prototype:',mi.__proto__);
console.log('Phone.prototype指向Object.prototype:',mi.__proto__.__proto__);
console.log('Objec.prototype指向null:',mi.__proto__.__proto__.__proto__);
// 構造函數的隱式原型鏈
console.log('Phone.__proto__指向Function.prototype',Phone.__proto__);
console.log('Function.prototype指向Object.prototype',Phone.__proto__.__proto__);
console.log('Object.prototype指向null',Phone.__proto__.__proto__.__proto__);
var obj = new Object();
// 一般創建對象的原型鏈
console.log('創建對象的__proto__',obj.__proto__);
console.log('Object.prototype指向null',obj.__proto__.__proto__);
function Object(){
}
console.log('指向Function.prototype',Object.__proto__);
console.log('Object.prototype',Object.__proto__.__proto__);
function Function(){
}
console.log(Function.__proto__);
console.log('Object.prototype',Function.__proto__.__proto__);
上面例子可以知道
- Object.prototype 對象的proto指向的是 null,特例
- xxx.prototype 是一個對象
- Function 和 Object 是由 Function 創建的,proto指向 Fcuntion.prototype
graph BT
Object.prototype-->|__proto__| null
Function.prototype-->|__proto__| Object.prototype
Phone.prototype-->|__proto__| Object.prototype
mi-->|__proto__| Phone.prototype
Phone-->|__proto__| Function.prototype
obj-->|__proto__| Object.prototype
Function-->|__proto__| Function.prototype
Object-->|__proto__| Function.prototype
arr-->|__proto__| Array.prototype
Array.prototype-->|__proto__| Object.prototype
- Function.prototype/Phone.prototype/obj/Array.prototype 原型對象都是由 function Object 創建的,所以指向 Object 的原型對象
- Phone,Function 和 Object 是由 function Function 創建的,所以指向 Function 的原型對象
- Object 是由 Fucntion 創建,而 Function 的原型對象是由 Object 創建
constructor 屬性
- constructor 屬性的值是一個函數對象
- 任意函數的 fuc.prototype.constructor === fuc
- 原型的 constructor 指向構造函數,實例 constructor 同樣指向構造函數
方法 1
function Ba(str) {
this.name = str ? str : 'baobo';
this.sayHello = function() {
alert('hello');
}
}
Ba.prototype = {
alertA:function (){
alert(this.name+'-A');
},
alertB: function() {
alert(this.name+'');
},
}
var instance_b = new Ba('haha');
// constructor
console.log('原型的構造函數', Ba.prototype.constructor); //function Object(){}
console.log('實例的構造函數', instance_b.constructor); //function Object(){}
// 新定義的原型對象,並不具有constructor屬性
console.log(Ba.prototype.hasOwnProperty('constructor')); //false
上面的方法相當於重寫 Ba.prototype 對象,新定義的原型對象不包含 constructor,因此構造函數指向的 function Object(){},需要顯式的給原型添加構造函數
雖然實例對象的 constructor 和構造函數原型的 constructor 都指向構造函數,但是實例對象並不具有 constructor 這個屬性,是繼承至 Ba.prototype
console.log(ba.hasOwnProperty('constructor')); //false
console.log(Ba.prototype.hasOwnProperty('constructor')); //true
graph LR
instance_b.constructor-->|實例的構造函數| Ba
Ba.prototype.constructor-->|原型對象的構造函數| Ba
修改
Ba.prototype = {
constructor: Ba,
alertA: function() {
alert(this.name + '-A');
},
alertB: function() {
alert(this.name + '');
},
}
<!--這樣就可正確指向構造函數Ba了-->
方法 2,直接在預定義的原型對象上擴展
function Ba(str) {
this.name = str ? str : 'baobo';
this.sayHello = function() {
alert('hello');
}
}
Ba.prototype.alertA = function() {
alert(this.name + '-A');
}
Ba.prototype.alertB = function() {
alert(this.name + 'B');
}
var instance_b = new Ba('haha');
// constructor
console.log('原型的構造函數', Ba.prototype.constructor); //f Ba(){}
console.log('實例的構造函數', instance_b.constructor); //f Ba(){}
graph TB
聲明構造函數Ba-->構造函數有prototype對象
構造函數有prototype對象-->prototype對象自動有constructor屬性
prototype對象自動有constructor屬性-->創建實例對象instance_b
創建實例對象instance_b-->繼承prototype,有instance_b.constructor
繼承prototype,有instance_b.constructor-->instance_b.constructor指向Ba
instance_b.constructor指向Ba-->對象有__proto__
對象有__proto__-->instance_b指向Ba.prototype
instanceof
- 檢測對象是否屬性某個類 A instanceof B(驗證原型對象與實例對象之間的關系 判斷具體類型)
- Instanceof 的判斷隊則是:沿着 A 的proto這條線來找,同時沿着 B 的prototype這條線來找,如果兩條線能找到同一個引用,即同一個對象,那么就返回 true.如果找到終點還未重合,則返回 false.
- instanceof 表示的就是一種繼承關系,或者原型鏈的結構
- instanceof 不光能找直接的父級,能找父級的父級的...constructor 只能找直接的父級
function a() {
this.name = 'alisy';
}
a.prototype.alertA = function () {
alert(this.name);
}
function b() {
this.name = 'baobo';
}
b.prototype.alertB = function () {
alert(this.name);
}
function c() {
this.name = 'cmen'
}
c.prototype = a.prototype;
//b得prototype對象指向一個c的實例,那么所有的b的實例就能繼承c
b.prototype = new c();
b.prototype.constructor = b;
var newb = new b();
var newc = new c();
newb.alertA(); //執行baobo
newc.alertA(); //執行cmen
//instanceof
console.log(b instanceof a);
console.log(b instanceof b);
繼承:父級有的,子級也有——給父級加東西,子級也有
- 第一種方法也是最簡單的方法,使用 call 或 apply 方法,將父對象的構造函數綁定在子對象上,即在子對象構造函數中加一行
Animal.apply(this, arguments);
Animal.call(this, arguments);
- 第二種方法更常見,使用 prototype 屬性.如果"貓"的 prototype 對象,指向一個 Animal 的實例,那么所有"貓"的實例,就能繼承 Animal 了.
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
call 方法
- 調用 call 的對象必須是個函數
- 語法:call([thisObj[,arg1[, arg2[, [,.argN]]]]])
- 定義:一個對象的一個方法調用 call,以 call 方法的第一個參數替換當前對象.
- 說明:call 方法可以用來代替另一個對象調用一個方法.call 方法可將一個函數的對象上下文從初始的上下文改變為由 thisObj 指定的新對象.
如果沒有提供 thisObj 參數,那么 Global 對象被用作 thisObj
apply 方法
- 語法:apply([thisObj[,argArray]])
- 定義:應用某一對象的一個方法,用另一個對象替換當前對象.
- 說明:如果 argArray 不是一個有效的數組或者不是 arguments 對象,那么將導致一個 TypeError.
如果沒有提供 argArray 和 thisObj 任何一個參數,那么 Global 對象將被用作 thisObj, 並且無法被傳遞任何參數.
實現繼承除了用 call 和 apply 還可以使用原型鏈實現
案例一
function add(a+b){
alert(a+b);
}
function sub(a,b){
alert(a-b);
}
add.call(sub,3,1);
//個人理解call和apply的作用就是切換函數的對象上下文
解:用括號的第一個參數來代替this的指向,將add的執行的上下文由window切換為sub,相當於this指向由window換成sub,add.call(sub,3,1) == add(3,1),結果為alert(4);
注意 : js中的函數是對象,函數名是對Function對象的引用
案例二
function Animal(){
this.name = "animal";
this.showName = function(){
alert(this.name);
}
}
function Cat(){
this.name = "cat";
}
var animal = new Animal();
var cat = new Cat();
//通過call()和apply(),將原本屬於Animal對象的方法showName交給Cat對象使用了,也就是將this指向Animal動態更改為Cat
//輸出的結果是cat
animal.showName.call(cat,"","");
//animal.showName.apply(cat,[]);
案例三:實現繼承
function Animal(name) {
this.name = name;
this.showName = function(name, a, b) {
console.log('this是:' + this.name + '\na是:' + a + '\nb是:' + b);
}
}
function Cat(name) {
Animal.call(this, name);
this.showLog = function() {
console.log('hello');
}
}
Cat.prototype.showAge = function() {
console.log('world');
}
var cat = new Cat('hello world');
cat.showName('abc', 12, 5); //可以直接調用showName()方法
注意:Animal.call(this);是使用Animal對象代替this對象,
this指向Animal,Cat就有了Animal對象中的方法和屬性,Cat對
象就可以直接調用Animal對象的方法和屬性
call第二個參數開始會映射到Animal相應的參數位置
案例四:多重繼承
function Animal() {
this.showSub = function(a, b) {
console.log(a - b);
}
}
function Cat() {
this.showAdd = function(a, b) {
console.log(a + b);
}
}
function Dog() {
Animal.call(this);
Cat.call(this);
}
var a = new Dog();
a.showSub(5,3);//2
a.showAdd(5,3);//8
使用兩個或者更多的call實現多重繼承
call和apply這兩個方法差不多,區別在於call的第二個參數是任意類型,而apply的第二個參數必須是數組,也可以是arguments
實現一些常見方法
實現 func.call(this,arg1,arg2...)
Function.prototype.call2 = function(context) {
console.log(arguments);
// 如果傳入的為null,則指向window
context = context || window;
// 函數調用的時候,this指向調用的函數
context.fn = this;
var args = [];
for (var i = 1, len = arguments.length; i < len; i++) {
args.push('arguments[' + i + ']');
}
// 解析字符串,執行其中的js代碼
// 獲取返回值
var result = eval('context.fn(' + args + ')');
// 執行完后將添加的屬性刪除
delete context.fn;
return result;
};
var heo = 'hello world';
var foo = {
name: 'lili'
}
function func(age, sex) {
console.log(age);
console.log(sex);
console.log(this.name);
return {
name: this.name,
age: age,
sex: sex,
}
}
func.call2(null);
console.log(func.call2(foo, 23, '男'))
實現 func.apply(this,[])
- 修改 func 函數的 this 指向
- 執行 func 函數
Function.prototype.newApply = function(context, arr) {
var result, i, len;
context = context || window;
context.fn = this;
if (!arr) {
result = context.fn;
} else {
var args = [];
for (i = 0, len = arr.length; i < len; i++) {
args.push('arr[' + i + ']');
}
result = eval('context.fn(' + args + ')');
}
delete context.fn;
return result;
}
var obj = {
name: 'alice'
}
function func(age, sex) {
console.log(age);
console.log(sex);
console.log(this.name);
return {
name: this.name
}
}
console.log(func.newApply(obj, [23, '女']));
實現一個 bind => newBind
寫程序的一個錯誤,this 丟失原先的對象,將對象的方法進行賦值之后再執行,於是變成 window.new_showA(),this 指向全局對象 window,
var a = 1;
var obj = {
a: 11,
showA: function () {
console.log(this.a);
}
};
obj.showA();
var new_showA = obj.showA;
new_showA(); //1
所以此時需要修改 this 指向
var new_showA = obj.showA.bind(this);
new_showA(); //1
實現 bind 方法
Function.prototype.newBind = function (context) {
//this是該函數的調用者
var self = this;
//arguments.slice(1),從1開始截取數組
var args = Array.prototype.slice.call(arguments, 1);
return function () {
// 第二個arguments是返回的函數的參數
var bindargs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindargs));
}
};
var a = 9;
var obj = {
a: 99,
showA: function (name, age) {
console.log(this.a);
console.log(name);
console.log(age);
}
};
obj.showA('alice', 12);
var new_showA = obj.showA.newBind(obj, 'lilith');
new_showA(23); //相當於執行obj.showA.apply(obj);
new 運算符的簡易實現
function Animal(name, age) {
this.name = name;
this.age = age;
this.voice = 'miao';
}
Animal.prototype.type = 'mao';
Animal.prototype.saying = function () {
console.log(this.voice);
};
var cat = _new(Animal, 'mimi', 10);
console.log('instance =>', cat);
console.log({
name: cat.name,
age: cat.age,
type: cat.type
});
cat.saying();
function _new() {
// 將偽數組arguments從頭部刪除一個,並將其其返回,此處是為了獲取傳入的構造函數
var Constructor = Array.prototype.shift.call(arguments);
// obj.__proto__指向創建obj的函數的原型,也就是function Object(){}的原型,obj是一個實例對象,沒有prototype屬性
var obj = Object.create(Constructor.prototype);
var result = Constructor.apply(obj, arguments);
// 如果構造函數有返回值,做一下處理,如果返回的是對象,就返回對象,否則該是什么就是什么
return typeof result === 'object' ? result : obj;
}
new 干了什么
- 獲取構造函數
- 通過 Object.create 生成新對象
<!--Object.create類似於-->
function Func(){};
Func.prototype = Constructor.prototype;
return new Func();
- 將 this 修改為新對象 obj,執行構造函數
- 將新對象返回