? 在js中,call和apply是二個神奇的方法,但同時也是容易令人迷惑的二個方法,call和apply的功能是以不同的對象作為上下文來調用某個函數的,簡而言之,就是允許一個對象去調用另一個對象的成員函數,咋一看似乎很不可思議,而且還容易引起混亂,但其實js并沒有嚴格的所謂’成員函數‘的概念,函數與對象的所屬關系在調用時才展現出來,靈活使用call和apply可以節省不少時間,在后面我們可以看到,call可以用于實現對象的繼承
? call和apply的功能是一致的,二者細微的差別在于call以參數表來接受被調用函數的參數,而apply以數組來接受被調用函數的參數,call和apply的語法分別是:
? ??
func.call(thisArg[, arg1[, arg2[, ... ]]])
func.apply(thisArg[, argsArray] )
? 其中,func是函數的引用,thisArg是func被調用時的山修改文對象,arg1,arg2或argsArray是傳入func的參數,我們以下面一段代碼為例介紹call的工作機制:
1 var someuser = { 2 name: 'by', 3 display: function (words) { 4 console.log(this.name + ' says ' + words); 5 } 6 }; 7 var foo = { 8 name: 'foo' 9 }; 10 someuser.display.call(foo, 'hello'); //輸出foo says hello
someuser.display是被調用的函數,它通過call將上下文改變為foo對象,因此在函數體內訪問的this.name時實際上訪問的就是foo.name,因此輸出的即是foo.
2bind
如何改變被調用函數的上下文呢?前面說過,可以用call或apply方法,但如果重復使用不方便,因此每次都要把上下文對象作為參數傳遞,而且還會使代碼變得不直觀,針對這種情況,我們可以使用bind方法來永久的綁定函數的上下文,使其無論被誰調用,上下文都是固定的,bind語法如下:
func.bind(thisArg[, arg1[, arg2[, ...]]])
?其中func是待綁定函數,thisArg是改變的上下文對象,arg1,arg2是綁定的參數表,bind方法返回值是上下文為thisArg的func,通過下面這個例子可以幫我們理解bind的使用方法:
1 var someuser = { 2 name: 'by', 3 func: function () { 4 console.log(this.name); 5 } 6 }; 7 var foo = { 8 name: 'foo' 9 }; 10 foo.func = someuser.func; 11 foo.func(); //輸出foo 12 13 foo.func1 = someuser.func.bind(someuser); 14 foo.func1(); //輸出by 15 16 func = someuser.func.bind(foo); 17 func(); //輸出foo 18 19 func2 = func; 20 func2(); //輸出foo
?上面代碼直接將foo.func 賦值給someuser.func,調用foo.func()時, this指針為foo,所以輸出結果是foo.
foo.func1使用了bind方法,將someuser作為this指針綁定到someuser.func,調用foo.func1()時,this指針為someuser,所以輸出結果是by.
全局函數func同樣使用了bind方法,將foo作為this指針綁定到someuser.func,調用func()時,this指針為foo,所以輸出結果是foo,而func2直接將綁定過的func賦值過來,與func行為完全相同
3使用 bind綁定參數表
bind方法還有一個重要功能,綁定參數表,如下例所示:
1 var person = { 2 name: 'by', 3 says: function (act, obj) { 4 console.log(this.name + ' ' + act + ' ' + obj); 5 } 6 }; 7 person.says('loves', 'diovyb'); //輸出by loves diovyb 8 9 bya = person.says.bind(person, 'loves'); 10 bya('you'); //輸出by loves you
? 可以看到,bya將this指針綁定到了person,并將第一個參數綁定到loves,之后在調用bya的時候,只需傳入第三個參數,這個特性可以用于創建一個函數的’捷徑‘,之后我們可以通過這個捷徑調用,以便在代碼多處調用時省略重復輸入相同的參數
4理解bind
盡管bind很優美,還是有一些令人疑惑的地方,例如下面的代碼:
1 var someuser = { 2 name: 'by', 3 func: function () { 4 console.log(this.name); 5 } 6 }; 7 var foo = { 8 name : 'foo' 9 }; 10 func = someuser.func.bind(foo); 11 func(); //輸出foo 12 13 func2 = func.bind(someuser); 14 func2(); //輸出foo
全局函數func通過someuser.func.bind將this指針綁定到了foo,調用func()輸出了foo,我們試圖將func2賦值為已綁定的func重新通過bind將this指針綁定到someuser的結果,而調用func2時的輸出卻竟然沒有像我們想像中的那樣變成by,這是為森么呢,繼續看下去,讓我為你揭秘
Bind方法的簡化版(不支持綁定參數表)
1 someuser.func.bind = function (self) { 2 return this.call(self); 3 };
? ?假設上面函數是someuser.func的bind方法的實現,函數體內的this指向的是someuser.func,因此函數也是對象,所以this.call(self)的作用就是以self作為this指針調用someuser.func
1 //將func = someuser.func.bind(foo) 展開 2 func = function () { 3 return someuser.func.call(foo); 4 }; 5 //再將func2 = func.bind(someuser) 展開 6 func2 = function () { 7 return func.call(someuser); 8 };
從上面的展開過程我們可以看出,func2實際上是以 someuser 作為func的this 指針調用了func,而func根本沒有使用this指針,所以二次bind是沒有效果的。
A5原型
原型是js面向對象中重要特性
1 function person() { 2 3 } 4 person.prototype.name = 'by'; 5 person.prototype.showName = function () { 6 console.log(this.name); 7 }; 8 9 var person = new person(); 10 person.showName();
上面這段代碼使用了原型而不是構造函數初始化對象,這樣做與直接在構造函數中定義屬性有什么不同呢?
1構造函數內定義的屬性繼承方式與原型不同,子對象需要顯示調用父對象才能繼承構造函數內部定義的屬性
2.構造函數內定義的任何屬性,包括函數在內都會被重復創建,同一個構造函數產生的二個對象不共享實例
3構造函數內定義的函數有運行時閉包的開銷,因為構造函數內的局部變量對其中定義的函數來說是可見的
下面的這段代碼可以驗證以上問題:
1 function foo() { 2 var inner = 'hello'; 3 this.prop1 = 'by'; 4 this.func1 = function () { 5 inner = ''; 6 }; 7 } 8 foo.prototype.prop2 = 'car'; 9 foo.prototype.func2 = function () { 10 console.log(this.prop2); 11 }; 12 var foo1 = new foo(); 13 var foo2 = new foo(); 14 console.log(foo1.func1 == foo2.func1); //輸出false 15 console.log(foo1.func2 == foo2.func2); //輸出 true
盡管如此,并不是說在構造函數內創建屬性不好,而是二者各自有適合的范圍,那么我們什么時候使用原型,什么時候使用構造函數來定義內部屬性
1除非必須用構造函數閉包,否則盡量用原型定義成員函數,因為這樣可以減少開銷
2.盡量在構造函數內定義一般成員,尤其是對象或數組,因為用原型定義的成員是多個實例共享的
接下來,我們來介紹js的原型鏈機制
? ?js中有二個特殊的對象:object與function,它們都是構造函數,用于生成對象,object.prototype是所有對象的祖先,function.prototype是所有函數的原型,包括構造函數,我把js中的對象分為三類:一類用戶創建的對象,一類是構造函數對象,一類是原型對象。用戶創建的對象,即一般意義上用new語句顯示構造的對象,構造函數對象指的是普通的構造函數,即通過new調用生成普通對象的函數,原型對象特指構造函數prototype屬性指向的對象,這三類對象中的每一類都有一個__p[roto__屬性,它指向該對象的原型,從任何對象沿著它開始遍歷都可以追溯到object.prototype,構造函數對象有prototype屬性,指向一個原型對象,通過該構造函數創建對象時,被創建對象的__proto__屬性將會指向構造函數的prototype屬性,原型對象有constructor屬性,指向它對應的構造函數,讓我們通過下面的例子來理解原型
1 function foo() { 2 3 } 4 object.prototype.name = 'my'; 5 foo.prototype.name = 'bar'; 6 7 var obj = new object(); 8 var foo = new foo(); 9 console.log(obj.name); //輸出my 10 console.log(foo.name); //輸出bar 11 console.log(foo.__proto__.name); //輸出bar 12 console.log(foo.__proto__.__proto__.name); //輸出my 13 console.log(foo.__proto__.constructor.prototype.name); //輸出bar
我們定義了一個叫做foo()的構造函數,生成對象foo.同時我們還分別給object和foo生成原型對象.
? ? 在js中,繼承是以依靠一套叫做原型鏈的機制實現的,屬性繼承的本質就是一個對象可以訪問到它的原型鏈上任何一個原型對象的屬性,例如上例的foo對象,它擁有foo.__Proto__和foo.__proto__.proto__所有屬性的淺拷貝(只復制基本數據類型,不復制對象),所以可以直接訪問foo.constructor(來自foo.__proto__,即foo.prototype), ?foo.tostring(來自foo.__proto__.__proto__,即object.prototype).
A6對象的復制
js中沒有像c語言一樣的指針,也沒有像java一樣的clone方法可以進行對象賦值,因此我們需要手動實現這樣一個函數,一個簡單的做法就是復制對象的所有屬性:
1 object.prototype.clone = function () { 2 var newobj = {}; 3 for (var i in this){ 4 newobj[i] = this[i]; 5 } 6 return newobj; 7 }; 8 var obj = { 9 name: 'by', 10 likes: ['node'] 11 }; 12 var newobj = obj.clone(); 13 obj.likes.push('python'); 14 15 console.log(obj.likes); //輸出['node','python'] 16 console.log(newobj.likes); //輸出['node','python']
?上面的代碼是一個對象淺拷貝的實現,即只復制基本類型的屬性,而共享對象類型的屬性,淺拷貝的問題是二個對象共享對象類型的屬性,例如上例中的likes屬性指向的是同一個數組
? 實現一個完全的復制,或深拷貝不是一件容易的事,因為除了基本數據類型,還有多種不同的對象,對象內部還有復雜的結構,因此需要用遞歸來實現
1 object.prototype.clone = function () { 2 var newobj = {}; 3 for (var i in this){ 4 if (typeof(this[i]) == 'object' || typeof(this[i] == 'function')){ 5 newobj[i] = this[i].clone(); 6 } else { 7 newobj[i] = this[i]; 8 } 9 } 10 return newobj; 11 }; 12 Array.prototype.clone = function () { 13 var newArray = []; 14 for (var i = 0; i< this.length;i++){ 15 if (typeof(this[i]) == 'object' || typeof(this[i] == 'function')){ 16 newArray[i] = this[i].clone(); 17 } else { 18 newArray[i] = this[i]; 19 } 20 } 21 return newArray; 22 }; 23 function.prototype.clone = function () { 24 var that = this; 25 var newfunc = function(){ 26 return that.apply(this, arguments); 27 }; 28 for (var i in this){ 29 newfunc[i] = this[i]; 30 } 31 return newfunc(); 32 }; 33 var obj = { 34 name: 'by', 35 likes: ['node'], 36 display: function(){ 37 console.log(this.name); 38 } 39 }; 40 var newobj = obj.clone(); 41 newobj.likes.push('python'); 42 console.log(obj.likes); //輸出['node'] 43 console.log(newobj.likes); //輸出['node','python'] 44 console.log(obj.display == newobj.display); //輸出false
上面這個辦法實現雖然看上去非常完美,它不僅遞歸的復制了對象復雜的結構,還實現了函數的深拷貝,這個方法在大多數的情況都好用,但是有一種情況它無能為力,例如下面的代碼
var obj1 = {ref: null }; var obj2 = {ref: obj1 }; obj1.ref = obj2;
這段代碼邏輯非常簡單,就是二個相互引用的對象,當我們試圖使用深拷貝來復制obj1和obj2中的任何一個時,問題就出現了,因為深拷貝的做法就是遇到對象就進行遞歸復制,那么結果只能無限循環下去,對于這種情況,簡單的遞歸已經無法解決,必須設計一套圖論算法,分析對象之間的依賴關系,建立一個拓撲結構圖,然后分別依次復制每個頂點,并重新構建它們之間的依賴關系,這個我們暫且不討論了,過于高深,而且在實際中我們也幾乎不會遇到這種情況,好的,js全部重要的就在這里了,讓我們下次再見!!!
?