面向對象編程(Object Oriented Programming,縮寫爲 OOP)web
是目前主流的編程範式。編程
是單個實物的抽象,數組
是一個容器,封裝了屬性(property)和方法(method),屬性是對象的狀態,方法是對象的行爲(完成某種任務)。瀏覽器
將真實世界各類複雜的關係,抽象爲一個個對象,而後由對象之間的分工與合做,完成對真實世界的模擬。安全
每個對象都是功能中心,具備明確分工,能夠完成接受信息、處理數據、發出信息等任務。數據結構
對象能夠複用,經過繼承機制還能夠定製。app
具備靈活、代碼可複用、高度模塊化等特色,容易維護和開發,比起由一系列函數或指令組成的傳統的過程式編程(procedural programming),更適合多人合做的大型軟件項目。模塊化
「類」 就是對象的模板,對象就是 「類」 的實例。函數
JavaScript 語言的對象體系,不是基於「類」的,而是基於構造函數(constructor)和原型鏈(prototype)。ui
this
關鍵字,表明所要生成的實例new
命令new
命令,讓構造函數生成一個實例對象new
命令執行時,構造函數內部的this
,就表明了新生成的實例對象new
命令時,根據須要,構造函數也能夠接受參數
var Vehicle = function (p) { this.price = p; }; var v = new Vehicle(500);
new
命令自己就能夠執行構造函數,因此後面的構造函數能夠帶括號,也能夠不帶括號。可是爲了表示是函數調用,推薦使用括號
var Vehicle = function (){ this.price = 1000; }; var v = Vehicle(); // 將構造函數當成普通函數調用,不抱錯 v // undefined // 可是變量v變成了undefined price // 1000 // price屬性變成了全局變量 // 一個解決辦法是: // 構造函數內部使用嚴格模式,即第一行加上use strict。 // 這樣的話,一旦忘了使用new命令,直接調用構造函數就會報錯。
function Fubar(foo, bar){ 'use strict'; this._foo = foo; // 因爲 嚴格模式中,函數內部的 this
不能指向全局對象,默認等於undefined
this._bar = bar; } Fubar(); // TypeError: Cannot set property '_foo' of undefined
// 另外一個解決辦法:
// 造函數內部判斷是否使用new
命令,若是發現沒有使用,則直接返回一個實例對象。
thisundefinednew
function Fubar(foo, bar) { if (!(this instanceof Fubar)) { return new Fubar(foo, bar); } this._foo = foo; this._bar = bar; }
// 此時無論加不加new
命令,都會獲得一樣的結果 Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1new
使用new
命令時,它後面的函數依次執行下面的步驟
1. 建立一個空對象,做爲將要返回的對象實例
2. 將這個空對象的隱式原型對象 __proto__,指向構造函數的 prototype 屬性
3. 將這個空對象賦值給函數內部的 this 關鍵字
4. 開始執行構造函數內部的代碼
this
關鍵字的函數)使用new
命令,則會返回一個空對象new
命令老是返回一個對象,要麼是實例對象,要麼是return
語句指定的對象
function getMessage() { return 'this is a message'; } var msg = new getMessage(); msg // {} typeof msg // "object"
new
命令簡化的內部流程,能夠用下面的代碼表示function _new(/* 構造函數 */ constructor, /* 構造函數參數 */ params) { // 將 arguments 對象轉爲數組 var args = [].slice.call(arguments); // 取出構造函數 var constructor = args.shift(); // 建立一個空對象,繼承構造函數的 prototype 屬性 var context = Object.create(constructor.prototype); // 執行構造函數 var result = constructor.apply(context, args); // 若是返回結果是對象,就直接返回,不然返回 context 對象 return (typeof result === 'object' && result != null) ? result : context; } // 實例 var actor = _new(Person, '張三', 28);
new
命令調用,new.target
指向當前函數,不然爲 undefined
function f() { console.log(new.target === f); } f(); // false // 指向 new f(); // true // 指向 當前函數new.targetundefinednew.target
new
命令
function f() { if (!new.target) { throw new Error('請使用 new 命令調用!'); } // ... } f(); // Uncaught Error: 請使用 new 命令調用!
var Person= { name: '張三', age: 38, greeting: function() { console.log('Hi! I\'m ' + this.name + '.'); } }; var person2 = Object.create(Person); person2.name; // 張三 person2.greeting(); // Hi! I'm 張三.
this除了能夠在構造函數中表示實例對象,還能夠用在其餘場合
可是無論什麼場合,this 老是返回一個對象
var A = { name: '張三', describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; // window.name var f = A.describe; // window.f f(); // "姓名:李四"
function f() { return '姓名:'+ this.name; } var A = { name: '張三', describe: f }; var B = { name: '李四', describe: f }; A.describe(); // "姓名:張三" B.describe(); // "姓名:李四"
<input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){ if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } </script>
// 一個文本輸入框,每當用戶輸入一個值,就會調用回調函數,驗證這個值是否在指定範圍。
// 瀏覽器會向回調函數傳入當前對象,所以 就表明傳入當前對象(即文本框),而後就能夠從 上面讀到用戶的輸入值onChangethisobj.value
JavaScript 支持運行環境動態切換,也就是說,this
的指向是動態的,沒有辦法事先肯定到底指向哪一個對象
var obj = { foo: 5 }; // 原始的對象以字典結構保存,每個屬性名都對應一個屬性描述對象。 // 舉例來講,上面例子的 foo 屬性,其實是如下面的形式保存的 { foo: { [[value]]: 5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } }
var f = function () { console.log(this.x); } var x = 1; var obj = { ff: f, x: 2, }; // 單獨執行 f(); // 1 // obj 環境執行 obj.ff(); // 2
var o = { f1: function () { console.log(this); var f2 = function () { console.log(this); }(); } } o.f1(); // Object // Window
var o = {
f1: function() {
console.log(this);
var that = this;
var f2 = function() {
console.log(that);
}();
}
}
o.f1()
// Object
// Object
// 以上代碼等價於
var temp = function () {
console.log(this);
};
var o = { f1: function () { console.log(this); var f2 = temp(); } }
map
和foreach
方法,容許提供一個函數做爲參數。這個函數內部不該該使用this
var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }); } } o.f() // undefined a1 // undefined a2 // 解決方法1 使用變量固定 this var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { var that = this; this.p.forEach(function (item) { console.log(that.v+' '+item); }); } } o.f() // hello a1 // hello a2 // 解決方法2 forEach( , this) var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }, this); } } o.f() // hello a1 // hello a2
var o = new Object(); o.f = function () { console.log(this === o); } // jQuery 的寫法 $('#button').on('click', o.f);
上面代碼中,點擊按鈕之後,控制檯會顯示false
。緣由是此時this
再也不指向o
對象,而是指向按鈕的 DOM 對象,由於f
方法是在按鈕對象的環境中被調用的。
JavaScript 提供了call
、apply
、bind
這三個方法,來切換/固定this
的指向
var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true
var f = function () { return this; }; f.call(5) // Number {[[PrimitiveValue]]: 5}
function add(a, b) { return a + b; } add.call(this, 1, 2); // 3
var obj = {}; obj.hasOwnProperty('toString'); // false // 覆蓋掉繼承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true; }; obj.hasOwnProperty('toString'); // true // hasOwnProperty是obj對象繼承的方法,若是這個方法一旦被覆蓋,就不會獲得正確結果 // 將hasOwnProperty方法的原始定義放到obj對象上執行,這樣不管obj上有沒有同名方法,都不會影響結果 Object.prototype.hasOwnProperty.call(obj, 'toString'); // false
var a = [10, 2, 4, 15, 9]; // 結合使用apply方法和Math.max方法,就能夠返回數組的最大元素 Math.max.apply(null, a); // 15
// 利用 Array構造函數 和 apply結合 // 將數組的空元素變成undefined Array.apply(null, ['a', ,'b']); // [ 'a', undefined, 'b' ]
空元素與undefined的差異在於,數組的forEach方法會跳過空元素,可是不會跳過undefined
// 前提是 //被處理的對象必須有length屬性 //以及相對應的數字鍵 Array.prototype.slice.apply({0: 1, length: 1}); // [1] Array.prototype.slice.apply({0: 1}); // [] Array.prototype.slice.apply({0: 1, length: 2}); // [1, undefined] Array.prototype.slice.apply({length: 1}); // [undefined]
var o = new Object(); o.f = function () { console.log(this === o); } var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; // jQuery 的寫法 $('#button').on('click', f); //點擊按鈕之後,控制檯將會顯示true。 // 因爲apply方法(或者call方法)不只綁定函數執行時所在的對象,還會當即執行函數,所以不得不把綁定語句寫在一個函數體內。 // 更簡潔的寫法是採用下面介紹的bind方法。
var d = new Date(); d.getTime() // 1481869925657 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object. // 由於getTime方法內部的this,綁定Date對象的實例,賦給變量print之後,內部的this已經不指向Date對象的實例了
bind方法能夠解決這個問題
var print = d.getTime.bind(d); print(); // 1481869925657
bind 方法將 getTime 方法內部的 this 綁定到 d 對象,這時就能夠安全地將這個方法賦值給其餘變量了
var counter = { count: 0, inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1
counter.inc() 方法被賦值給變量 func 。這時必須用 bind 方法將 inc 內部的 this,綁定到 counter,不然就會出錯
var counter = { count: 0, inc: function () { this.count++; } }; var obj = { count: 100 }; var func = counter.inc.bind(obj); func(); obj.count // 101
var add = function (x, y) { return x * this.m + y * this.n; } var obj = { m: 2, n: 2 }; var newAdd = add.bind(obj, 5); newAdd(5) // 20
bind() 方法除了綁定 this 對象,還將 add 函數的第一個參數 x 綁定成 5,而後返回一個新函數 newAdd(),這個函數只要再接受一個參數 y 就能運行了
function add(x, y) { return x + y; } var plus5 = add.bind(null, 5); plus5(10); // 15
上面代碼中,函數 add 內部並無 this,使用 bind() 方法的主要目的是綁定參數 x,之後每次運行新函數 plus5,就只須要提供另外一個參數 y 就夠了。
並且由於 add 內部沒有 this,因此 bind() 的第一個參數是 null,不過這裏若是是其餘對象,也沒有影響
element.addEventListener('click', o.m.bind(o)); // 上面代碼中,click事件綁定bind方法生成的一個匿名函數。 // 這樣會致使沒法取消綁定,因此,下面的代碼是無效的 element.removeEventListener('click', o.m.bind(o)); // 正確寫法 var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener);
var counter = { count: 0, inc: function () { 'use strict'; this.count++; } }; function callIt(callback) { callback(); }
// 上面代碼中,callIt方法會調用回調函數。
// 這時若是直接把counter.inc傳入,調用時counter.inc內部的this就會指向全局對象。 callIt(counter.inc.bind(counter)); // 使用bind方法將counter.inc綁定counter之後,就不會有這個問題,this老是指向counter counter.count // 1
this
指向,極可能也會出錯
var obj = { name: '張三', times: [1, 2, 3], print: function () { this.times.forEach(function (n) { console.log(this.name); }); } }; obj.print(); // 沒有任何輸出 // obj.print內部this.times的this是指向obj的,這個沒有問題。 // 可是,forEach方法的回調函數內部的this.name倒是指向全局對象,致使沒有辦法取到值。 obj.print = function () { this.times.forEach(function (n) { console.log(this === window); }); }; obj.print() // true // true // true
解決方法
// 經過bind方法綁定this obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 張三 // 張三 // 張三
[1, 2, 3].slice(0, 1); // [1] // 等同於 Array.prototype.slice.call([1, 2, 3], 0, 1); // [1]
call方法實質上是調用Function.prototype.call方法,所以上面的表達式能夠用bind方法改寫
var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1); // [1]
var push = Function.prototype.call.bind(Array.prototype.push); var pop = Function.prototype.call.bind(Array.prototype.pop); var a = [1 ,2 ,3]; push(a, 4) a // [1, 2, 3, 4] pop(a) a // [1, 2, 3]
function f() { console.log(this.v); } var o = { v: 123 }; var bind = Function.prototype.call.bind(Function.prototype.bind); bind(f, o)(); // 123
因此bind
方法就能夠直接使用,不須要在函數實例上使用