JavaScript 面向?qū)ο蟾攀?/h1>

2021-09-15 15:14 更新

簡介

對(duì)象和面向?qū)ο缶幊?/h3>

“面向?qū)ο缶幊獭保∣bject Oriented Programming,縮寫為OOP)是目前主流的編程范式。它的核心思想是將真實(shí)世界中各種復(fù)雜的關(guān)系,抽象為一個(gè)個(gè)對(duì)象,然后由對(duì)象之間的分工與合作,完成對(duì)真實(shí)世界的模擬。

傳統(tǒng)的計(jì)算機(jī)程序由一系列函數(shù)或一系列指令組成,而面向?qū)ο缶幊痰某绦蛴梢幌盗袑?duì)象組成。每一個(gè)對(duì)象都是功能中心,具有明確分工,可以完成接受信息、處理數(shù)據(jù)、發(fā)出信息等任務(wù)。因此,面向?qū)ο缶幊叹哂徐`活性、代碼的可重用性、模塊性等特點(diǎn),容易維護(hù)和開發(fā),非常適合多人合作的大型軟件項(xiàng)目。

那么,“對(duì)象”(object)到底是什么?

我們從兩個(gè)層次來理解。

(1)“對(duì)象”是單個(gè)實(shí)物的抽象。

一本書、一輛汽車、一個(gè)人都可以是“對(duì)象”,一個(gè)數(shù)據(jù)庫、一張網(wǎng)頁、一個(gè)與遠(yuǎn)程服務(wù)器的連接也可以是“對(duì)象”。當(dāng)實(shí)物被抽象成“對(duì)象”,實(shí)物之間的關(guān)系就變成了“對(duì)象”之間的關(guān)系,從而就可以模擬現(xiàn)實(shí)情況,針對(duì)“對(duì)象”進(jìn)行編程。

(2)“對(duì)象”是一個(gè)容器,封裝了“屬性”(property)和“方法”(method)。

所謂“屬性”,就是對(duì)象的狀態(tài);所謂“方法”,就是對(duì)象的行為(完成某種任務(wù))。比如,我們可以把動(dòng)物抽象為animal對(duì)象,“屬性”記錄具體是那一種動(dòng)物,“方法”表示動(dòng)物的某種行為(奔跑、捕獵、休息等等)。

雖然不同于傳統(tǒng)的面向?qū)ο缶幊陶Z言,但是JavaScript具有很強(qiáng)的面向?qū)ο缶幊棠芰?。本章介紹JavaScript如何進(jìn)行“面向?qū)ο缶幊獭薄?/p>

構(gòu)造函數(shù)

“面向?qū)ο缶幊獭钡牡谝徊?,就是要生成?duì)象。

前面說過,“對(duì)象”是單個(gè)實(shí)物的抽象。所以,通常需要一個(gè)模板,表示某一類實(shí)物的共同特征,然后“對(duì)象”根據(jù)這個(gè)模板生成。

典型的面向?qū)ο缶幊陶Z言(比如C++和Java),存在“類”(class)這樣一個(gè)概念。所謂“類”就是對(duì)象的模板,對(duì)象就是“類”的實(shí)例。JavaScript語言沒有“類”,而改用構(gòu)造函數(shù)(constructor)作為對(duì)象的模板。

所謂“構(gòu)造函數(shù)”,就是專門用來生成“對(duì)象”的函數(shù)。它提供模板,作為對(duì)象的基本結(jié)構(gòu)。一個(gè)構(gòu)造函數(shù),可以生成多個(gè)對(duì)象,這些對(duì)象都有相同的結(jié)構(gòu)。

構(gòu)造函數(shù)是一個(gè)正常的函數(shù),但是它的特征和用法與普通函數(shù)不一樣。下面就是一個(gè)構(gòu)造函數(shù):

var Vehicle = function() {
  this.price = 1000;
};

上面代碼中,Vehicle就是構(gòu)造函數(shù),它提供模板,用來生成車輛對(duì)象。

構(gòu)造函數(shù)的最大特點(diǎn)就是,函數(shù)體內(nèi)部使用了this關(guān)鍵字,代表了所要生成的對(duì)象實(shí)例。生成對(duì)象的時(shí)候,必需用new命令,調(diào)用Vehicle函數(shù)。

new命令

new命令的作用,就是執(zhí)行構(gòu)造函數(shù),返回一個(gè)實(shí)例對(duì)象。

var Vehicle = function (){
  this.price = 1000;
};

var v = new Vehicle();
v.price // 1000

上面代碼通過new命令,讓構(gòu)造函數(shù)Vehicle生成一個(gè)實(shí)例對(duì)象,保存在變量v中。這個(gè)新生成的實(shí)例對(duì)象,從構(gòu)造函數(shù)Vehicle繼承了price屬性。在new命令執(zhí)行時(shí),構(gòu)造函數(shù)內(nèi)部的this,就代表了新生成的實(shí)例對(duì)象,this.price表示實(shí)例對(duì)象有一個(gè)price屬性,它的值是1000。

使用new命令時(shí),根據(jù)需要,構(gòu)造函數(shù)也可以接受參數(shù)。

var Vehicle = function (p){
  this.price = p;
};

var v = new Vehicle(500);

new命令本身就可以執(zhí)行構(gòu)造函數(shù),所以后面的構(gòu)造函數(shù)可以帶括號(hào),也可以不帶括號(hào)。下面兩行代碼是等價(jià)的。

var v = new Vehicle();
var v = new Vehicle;

一個(gè)很自然的問題是,如果忘了使用new命令,直接調(diào)用構(gòu)造函數(shù)會(huì)發(fā)生什么事?

這種情況下,構(gòu)造函數(shù)就變成了普通函數(shù),并不會(huì)生成實(shí)例對(duì)象。而且由于下面會(huì)說到的原因,this這時(shí)代表全局對(duì)象,將造成一些意想不到的結(jié)果。

var Vehicle = function (){
  this.price = 1000;
};

var v = Vehicle();
v.price
// Uncaught TypeError: Cannot read property 'price' of undefined

price
// 1000

上面代碼中,調(diào)用Vehicle構(gòu)造函數(shù)時(shí),忘了加上new命令。結(jié)果,price屬性變成了全局變量,而變量v變成了undefined。

因此,應(yīng)該非常小心,避免出現(xiàn)不使用new命令、直接調(diào)用構(gòu)造函數(shù)的情況。為了保證構(gòu)造函數(shù)必須與new命令一起使用,一個(gè)解決辦法是,在構(gòu)造函數(shù)內(nèi)部使用嚴(yán)格模式,即第一行加上use strict

function Fubar(foo, bar){
  "use strict";

  this._foo = foo;
  this._bar = bar;
}

Fubar()
// TypeError: Cannot set property '_foo' of undefined

上面代碼的Fubar為構(gòu)造函數(shù),use strict命令保證了該函數(shù)在嚴(yán)格模式下運(yùn)行。由于在嚴(yán)格模式中,函數(shù)內(nèi)部的this不能指向全局對(duì)象,默認(rèn)等于undefined,導(dǎo)致不加new調(diào)用會(huì)報(bào)錯(cuò)(JavaScript不允許對(duì)undefined添加屬性)。

另一個(gè)解決辦法,是在構(gòu)造函數(shù)內(nèi)部判斷是否使用new命令,如果發(fā)現(xiàn)沒有使用,則直接返回一個(gè)實(shí)例對(duì)象。

function Fubar(foo, bar){
  if (!(this instanceof Fubar)) {
    return new Fubar(foo, bar);
  }

  this._foo = foo;
  this._bar = bar;
}

Fubar(1, 2)._foo // 1
(new Fubar(1, 2))._foo // 1

上面代碼中的構(gòu)造函數(shù),不管加不加new命令,都會(huì)得到同樣的結(jié)果。

new命令的原理

使用new命令時(shí),它后面的函數(shù)調(diào)用就不是正常的調(diào)用,而是被new命令控制了。內(nèi)部的流程是,先創(chuàng)造一個(gè)空對(duì)象,作為上下文對(duì)象,賦值給函數(shù)內(nèi)部的this關(guān)鍵字。也就是說,this指的是一個(gè)新生成的空對(duì)象,所有針對(duì)this的操作,都會(huì)發(fā)生在這個(gè)空對(duì)象上。

構(gòu)造函數(shù)之所以叫“構(gòu)造函數(shù)”,就是說這個(gè)函數(shù)的目的,就是操作上下文對(duì)象(即this對(duì)象),將其“構(gòu)造”為需要的樣子。如果構(gòu)造函數(shù)的return語句返回的是對(duì)象,new命令會(huì)返回return語句指定的對(duì)象;否則,就會(huì)不管return語句,返回構(gòu)造后的上下文對(duì)象。

var Vehicle = function (){
  this.price = 1000;
  return 1000;
};

(new Vehicle()) === 1000
// false

上面代碼中,Vehicle是一個(gè)構(gòu)造函數(shù),它的return語句返回一個(gè)數(shù)值。這時(shí),new命令就會(huì)忽略這個(gè)return語句,返回“構(gòu)造”后的this對(duì)象。

但是,如果return語句返回的是一個(gè)跟this無關(guān)的新對(duì)象,new命令會(huì)返回這個(gè)新對(duì)象,而不是this對(duì)象。這一點(diǎn)需要特別引起注意。

var Vehicle = function (){
  this.price = 1000;
  return { price: 2000 };
};

(new Vehicle()).price
// 2000

上面代碼中,構(gòu)造函數(shù)Vehicle的return語句,返回的是一個(gè)新對(duì)象。new命令會(huì)返回這個(gè)對(duì)象,而不是this對(duì)象。

new命令簡化的內(nèi)部流程,可以用下面的代碼表示。

function _new(/* constructor, param, ... */) {
  var args = [].slice.call(arguments);
  var constructor = args.shift();
  var context = Object.create(constructor.prototype);
  var result = constructor.apply(context, args);
  return (typeof result === 'object' && result != null) ? result : context;
}

var actor = _new(Person, "張三", 28);

instanceof運(yùn)算符

instanceof運(yùn)算符用來確定一個(gè)對(duì)象是否為某個(gè)構(gòu)造函數(shù)的實(shí)例。

var v = new Vehicle();

v instanceof Vehicle
// true

instanceof運(yùn)算符的左邊放置對(duì)象,右邊放置構(gòu)造函數(shù)。在JavaScript之中,只要是對(duì)象,就有對(duì)應(yīng)的構(gòu)造函數(shù)。因此,instanceof運(yùn)算符可以用來判斷值的類型。

[1, 2, 3] instanceof Array // true

({}) instanceof Object // true

上面代碼表示數(shù)組和對(duì)象則分別是Array對(duì)象和Object對(duì)象的實(shí)例。最后那一行的空對(duì)象外面,之所以要加括號(hào),是因?yàn)槿绻患樱琂avaScript引擎會(huì)把一對(duì)大括號(hào)解釋為一個(gè)代碼塊,而不是一個(gè)對(duì)象,從而導(dǎo)致這一行代碼被解釋為“{}; instanceof Object”,引擎就會(huì)報(bào)錯(cuò)。

需要注意的是,由于原始類型的值不是對(duì)象,所以不能使用instanceof運(yùn)算符判斷類型。

"" instanceof String // false
1 instanceof Number // false

上面代碼中,字符串不是String對(duì)象的實(shí)例(因?yàn)樽址皇菍?duì)象),數(shù)值1也不是Number對(duì)象的實(shí)例(因?yàn)閿?shù)值1不是對(duì)象)。

如果存在繼承關(guān)系,也就是某個(gè)對(duì)象可能是多個(gè)構(gòu)造函數(shù)的實(shí)例,那么instanceof運(yùn)算符對(duì)這些構(gòu)造函數(shù)都返回true。

var a = [];

a instanceof Array // true
a instanceof Object // true

上面代碼表示,a是一個(gè)數(shù)組,所以它是Array的實(shí)例;同時(shí),a也是一個(gè)對(duì)象,所以它也是Object的實(shí)例。

利用instanceof運(yùn)算符,還可以巧妙地解決,調(diào)用構(gòu)造函數(shù)時(shí),忘了加new命令的問題。

function Fubar (foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  }
  else return new Fubar(foo, bar);
}

上面代碼使用instanceof運(yùn)算符,在函數(shù)體內(nèi)部判斷this關(guān)鍵字是否為構(gòu)造函數(shù)Fubar的實(shí)例。如果不是,就表明忘了加new命令。

this關(guān)鍵字

涵義

構(gòu)造函數(shù)內(nèi)部需要用到this關(guān)鍵字。那么,this關(guān)鍵字到底是什么意思呢?

簡單說,this就是指函數(shù)當(dāng)前的運(yùn)行環(huán)境。在JavaScript語言之中,所有函數(shù)都是在某個(gè)運(yùn)行環(huán)境之中運(yùn)行,this就是這個(gè)環(huán)境。對(duì)于JavaScipt語言來說,一切皆對(duì)象,運(yùn)行環(huán)境也是對(duì)象,所以可以理解成,所有函數(shù)總是在某個(gè)對(duì)象之中運(yùn)行,this就指向這個(gè)對(duì)象。這本來并不會(huì)讓用戶糊涂,但是JavaScript支持運(yùn)行環(huán)境動(dòng)態(tài)切換,也就是說,this的指向是動(dòng)態(tài)的,沒有辦法事先確定到底指向哪個(gè)對(duì)象,這才是最讓初學(xué)者感到困惑的地方。

舉例來說,有一個(gè)函數(shù)f,它同時(shí)充當(dāng)a對(duì)象和b對(duì)象的方法。JavaScript允許函數(shù)f的運(yùn)行環(huán)境動(dòng)態(tài)切換,即一會(huì)屬于a對(duì)象,一會(huì)屬于b對(duì)象,這就要靠this關(guān)鍵字來辦到。

function f(){ console.log(this.x); };

var a = {x:'a'};
a.m = f;

var b = {x:'b'};
b.m = f;

a.m() // a
b.m() // b

上面代碼中,函數(shù)f可以打印出當(dāng)前運(yùn)行環(huán)境中x變量的值。當(dāng)f屬于a對(duì)象時(shí),this指向a;當(dāng)f屬于b對(duì)象時(shí),this指向b,因此打印出了不同的值。由于this的指向可變,所以可以手動(dòng)切換運(yùn)行環(huán)境,以達(dá)到某種特定的目的。

前面說過,所謂“運(yùn)行環(huán)境”就是對(duì)象,this指函數(shù)運(yùn)行時(shí)所在的那個(gè)對(duì)象。如果一個(gè)函數(shù)在全局環(huán)境中運(yùn)行,this就是指頂層對(duì)象(瀏覽器中為window對(duì)象);如果一個(gè)函數(shù)作為某個(gè)對(duì)象的方法運(yùn)行,this就是指那個(gè)對(duì)象。

可以近似地認(rèn)為,this是所有函數(shù)運(yùn)行時(shí)的一個(gè)隱藏參數(shù),決定了函數(shù)的運(yùn)行環(huán)境。

使用場合

this的使用可以分成以下幾個(gè)場合。

(1)全局環(huán)境

在全局環(huán)境使用this,它指的就是頂層對(duì)象window。

this === window // true 

function f() {
    console.log(this === window); // true
}

上面代碼說明,不管是不是在函數(shù)內(nèi)部,只要是在全局環(huán)境下運(yùn)行,this就是指全局對(duì)象window。

(2)構(gòu)造函數(shù)

構(gòu)造函數(shù)中的this,指的是實(shí)例對(duì)象。

var O = function(p) {
    this.p = p;
};

O.prototype.m = function() {
    return this.p;
};

上面代碼定義了一個(gè)構(gòu)造函數(shù)O。由于this指向?qū)嵗龑?duì)象,所以在構(gòu)造函數(shù)內(nèi)部定義this.p,就相當(dāng)于定義實(shí)例對(duì)象有一個(gè)p屬性;然后m方法可以返回這個(gè)p屬性。

var o = new O("Hello World!");

o.p // "Hello World!"
o.m() // "Hello World!"

(3)對(duì)象的方法

當(dāng)a對(duì)象的方法被賦予b對(duì)象,該方法就變成了普通函數(shù),其中的this就從指向a對(duì)象變成了指向b對(duì)象。這就是this取決于運(yùn)行時(shí)所在的對(duì)象的含義,所以要特別小心。如果將某個(gè)對(duì)象的方法賦值給另一個(gè)對(duì)象,會(huì)改變this的指向。

var o1 = new Object();
o1.m = 1;
o1.f = function (){ console.log(this.m);};

o1.f() // 1

var o2 = new Object();
o2.m = 2;
o2.f = o1.f

o2.f() // 2

從上面代碼可以看到,f是o1的方法,但是如果在o2上面調(diào)用這個(gè)方法,f方法中的this就會(huì)指向o2。這就說明JavaScript函數(shù)的運(yùn)行環(huán)境完全是動(dòng)態(tài)綁定的,可以在運(yùn)行時(shí)切換。

如果不想改變this的指向,可以將o2.f改寫成下面這樣。

o2.f = function (){ o1.f() };

o2.f() // 1

上面代碼表示,由于f方法這時(shí)是在o1下面運(yùn)行,所以this就指向o1。

有時(shí),某個(gè)方法位于多層對(duì)象的內(nèi)部,這時(shí)如果為了簡化書寫,把該方法賦值給一個(gè)變量,往往會(huì)得到意想不到的結(jié)果。

var a = {
        b : {
            m : function() {
                console.log(this.p);
            },
            p : 'Hello'
        }
};

var hello = a.b.m;
hello() // undefined

上面代碼表示,m屬于多層對(duì)象內(nèi)部的一個(gè)方法。為求簡寫,將其賦值給hello變量,結(jié)果調(diào)用時(shí),this指向了全局對(duì)象。為了避免這個(gè)問題,可以只將m所在的對(duì)象賦值給hello,這樣調(diào)用時(shí),this的指向就不會(huì)變。

var hello = a.b;
hello.m() // Hello

(4)Node.js

在Node.js中,this的指向又分成兩種情況。全局環(huán)境中,this指向全局對(duì)象global;模塊環(huán)境中,this指向module.exports。

// 全局環(huán)境
this === global // true

// 模塊環(huán)境
this === module.exports // true

使用注意點(diǎn)

(1)避免多層this

由于this的指向是不確定的,所以切勿在函數(shù)中包含多層的this。

var o = {
    f1: function() {
        console.log(this); 
        var f2 = function() {
            console.log(this);
        }();
    }
}

o.f1()
// Object
// Window

上面代碼包含兩層this,結(jié)果運(yùn)行后,第一層指向該對(duì)象,第二層指向全局對(duì)象。一個(gè)解決方法是在第二層改用一個(gè)指向外層this的變量。

var o = {
    f1: function() {
        console.log(this); 
        var that = this;
        var f2 = function() {
            console.log(that);
        }();
    }
}

o.f1()
// Object
// Object

上面代碼定義了變量that,固定指向外層的this,然后在內(nèi)層使用that,就不會(huì)發(fā)生this指向的改變。

(2)避免數(shù)組處理方法中的this

數(shù)組的map和foreach方法,允許提供一個(gè)函數(shù)作為參數(shù)。這個(gè)函數(shù)內(nèi)部不應(yīng)該使用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

上面代碼中,foreach方法的參數(shù)函數(shù)中的this,其實(shí)是指向window對(duì)象,因此取不到o.v的值。

解決這個(gè)問題的一種方法,是使用中間變量。

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

另一種方法是將this當(dāng)作foreach方法的第二個(gè)參數(shù),固定它的運(yùn)行環(huán)境。

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

(3)避免回調(diào)函數(shù)中的this

回調(diào)函數(shù)中的this往往會(huì)改變指向,最好避免使用。

var o = new Object();

o.f = function (){
    console.log(this === o);
}

o.f() // true

上面代碼表示,如果調(diào)用o對(duì)象的f方法,其中的this就是指向o對(duì)象。

但是,如果將f方法指定給某個(gè)按鈕的click事件,this的指向就變了。

$("#button").on("click", o.f);

點(diǎn)擊按鈕以后,控制臺(tái)會(huì)顯示false。原因是此時(shí)this不再指向o對(duì)象,而是指向按鈕的DOM對(duì)象,因?yàn)閒方法是在按鈕對(duì)象的環(huán)境中被調(diào)用的。這種細(xì)微的差別,很容易在編程中忽視,導(dǎo)致難以察覺的錯(cuò)誤。

為了解決這個(gè)問題,可以采用下面的一些方法對(duì)this進(jìn)行綁定,也就是使得this固定指向某個(gè)對(duì)象,減少不確定性。

固定this的方法

this的動(dòng)態(tài)切換,固然為JavaScript創(chuàng)造了巨大的靈活性,但也使得編程變得困難和模糊。有時(shí),需要把this固定下來,避免出現(xiàn)意想不到的情況。JavaScript提供了call、apply、bind這三個(gè)方法,來切換/固定this的指向。

call方法

函數(shù)的call方法,可以指定該函數(shù)內(nèi)部this的指向(即函數(shù)執(zhí)行時(shí)所在的作用域),然后在所指定的作用域中,調(diào)用該函數(shù)。

var o = {};

var f = function (){
  return this;
};

f() === this // true
f.call(o) === o // true

上面代碼中,在全局環(huán)境運(yùn)行函數(shù)f時(shí),this指向全局環(huán)境;call方法可以改變this的指向,指定this指向?qū)ο髈,然后在對(duì)象o的作用域中運(yùn)行函數(shù)f。

再看一個(gè)例子。

var n = 123;
var o = { n : 456 };

function a() {
  console.log(this.n);
}

a.call() // 123
a.call(null) // 123
a.call(undefined) // 123
a.call(window) // 123
a.call(o) // 456

上面代碼中,a函數(shù)中的this關(guān)鍵字,如果指向全局對(duì)象,返回結(jié)果為123。如果使用call方法將this關(guān)鍵字指向o對(duì)象,返回結(jié)果為456??梢钥吹?,如果call方法沒有參數(shù),或者參數(shù)為null或undefined,則等同于指向全局對(duì)象。

call方法的完整使用格式如下。

func.call(thisValue, arg1, arg2, ...)

它的第一個(gè)參數(shù)就是this所要指向的那個(gè)對(duì)象,后面的參數(shù)則是函數(shù)調(diào)用時(shí)所需的參數(shù)。

function add(a,b) {
  return a+b;
}

add.call(this,1,2) // 3

上面代碼中,call方法指定函數(shù)add在當(dāng)前環(huán)境(對(duì)象)中運(yùn)行,并且參數(shù)為1和2,因此函數(shù)add運(yùn)行后得到3。

call方法的一個(gè)應(yīng)用是調(diào)用對(duì)象的原生方法。

var obj = {};
obj.hasOwnProperty('toString') // false

obj.hasOwnProperty = function (){
  return true;
};
obj.hasOwnProperty('toString') // true

Object.prototype.hasOwnProperty.call(obj, 'toString') // false

上面代碼中,hasOwnProperty是obj對(duì)象繼承的方法,如果這個(gè)方法一旦被覆蓋,就不會(huì)得到正確結(jié)果。call方法可以解決這個(gè)方法,它將hasOwnProperty方法的原始定義放到obj對(duì)象上執(zhí)行,這樣無論obj上有沒有同名方法,都不會(huì)影響結(jié)果。

apply方法

apply方法的作用與call方法類似,也是改變this指向,然后再調(diào)用該函數(shù)。唯一的區(qū)別就是,它接收一個(gè)數(shù)組作為函數(shù)執(zhí)行時(shí)的參數(shù),使用格式如下。

func.apply(thisValue, [arg1, arg2, ...])

apply方法的第一個(gè)參數(shù)也是this所要指向的那個(gè)對(duì)象,如果設(shè)為null或undefined,則等同于指定全局對(duì)象。第二個(gè)參數(shù)則是一個(gè)數(shù)組,該數(shù)組的所有成員依次作為參數(shù),傳入原函數(shù)。原函數(shù)的參數(shù),在call方法中必須一個(gè)個(gè)添加,但是在apply方法中,必須以數(shù)組形式添加。

請(qǐng)看下面的例子。

function f(x,y){
  console.log(x+y);
}

f.call(null,1,1) // 2
f.apply(null,[1,1]) // 2

上面的f函數(shù)本來接受兩個(gè)參數(shù),使用apply方法以后,就變成可以接受一個(gè)數(shù)組作為參數(shù)。

利用這一點(diǎn),可以做一些有趣的應(yīng)用。

(1)找出數(shù)組最大元素

JavaScript不提供找出數(shù)組最大元素的函數(shù)。結(jié)合使用apply方法和Math.max方法,就可以返回?cái)?shù)組的最大元素。

var a = [10, 2, 4, 15, 9];

Math.max.apply(null, a)
// 15

(2)將數(shù)組的空元素變?yōu)閡ndefined

通過apply方法,利用Array構(gòu)造函數(shù)將數(shù)組的空元素變成undefined。

Array.apply(null, ["a",,"b"])
// [ 'a', undefined, 'b' ]

空元素與undefined的差別在于,數(shù)組的foreach方法會(huì)跳過空元素,但是不會(huì)跳過undefined。因此,遍歷內(nèi)部元素的時(shí)候,會(huì)得到不同的結(jié)果。

var a = ["a",,"b"];

function print(i) {
  console.log(i);
}

a.forEach(print)
// a
// b

Array.apply(null,a).forEach(print)
// a
// undefined
// b

(3)轉(zhuǎn)換類似數(shù)組的對(duì)象

另外,利用數(shù)組對(duì)象的slice方法,可以將一個(gè)類似數(shù)組的對(duì)象(比如arguments對(duì)象)轉(zhuǎn)為真正的數(shù)組。

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]

上面代碼的apply方法的參數(shù)都是對(duì)象,但是返回結(jié)果都是數(shù)組,這就起到了將對(duì)象轉(zhuǎn)成數(shù)組的目的。從上面代碼可以看到,這個(gè)方法起作用的前提是,被處理的對(duì)象必須有l(wèi)ength屬性,以及相對(duì)應(yīng)的數(shù)字鍵。

(4)綁定回調(diào)函數(shù)的對(duì)象

上一節(jié)按鈕點(diǎn)擊事件的例子,可以改寫成

var o = new Object();

o.f = function (){
    console.log(this === o);
}

var f = function (){
  o.f.apply(o);
  // 或者 o.f.call(o);
};

$("#button").on("click", f);

點(diǎn)擊按鈕以后,控制臺(tái)將會(huì)顯示true。由于apply方法(或者call方法)不僅綁定函數(shù)執(zhí)行時(shí)所在的對(duì)象,還會(huì)立即執(zhí)行函數(shù),因此不得不把綁定語句寫在一個(gè)函數(shù)體內(nèi)。更簡潔的寫法是采用下面介紹的bind方法。

bind方法

bind方法用于將函數(shù)體內(nèi)的this綁定到某個(gè)對(duì)象,然后返回一個(gè)新函數(shù)。它的使用格式如下。

func.bind(thisValue, arg1, arg2,...)

下面是一個(gè)例子。

var o1 = new Object();
o1.p = 123;
o1.m = function (){
    console.log(this.p);
};

o1.m() // 123 

var o2 = new Object();
o2.p = 456;
o2.m = o1.m;

o2.m() // 456

o2.m = o1.m.bind(o1);
o2.m() // 123

上面代碼使用bind方法將o1.m方法綁定到o1以后,在o2對(duì)象上調(diào)用o1.m的時(shí)候,o1.m函數(shù)體內(nèi)部的this.p就不再到o2對(duì)象去尋找p屬性的值了。

bind比call方法和apply方法更進(jìn)一步的是,除了綁定this以外,還可以綁定原函數(shù)的參數(shù)。

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對(duì)象,還綁定了add函數(shù)的第一個(gè)參數(shù),結(jié)果newAdd函數(shù)只要一個(gè)參數(shù)就能運(yùn)行了。

如果bind方法的第一個(gè)參數(shù)是null或undefined,等于將this綁定到全局對(duì)象,函數(shù)運(yùn)行時(shí)this指向全局對(duì)象(在瀏覽器中為window)。

function add(x,y) { return x+y; }

var plus5 = add.bind(null, 5);

plus5(10) // 15

上面代碼除了將add函數(shù)的運(yùn)行環(huán)境綁定為全局對(duì)象,還將add函數(shù)的第一個(gè)參數(shù)綁定為5,然后返回一個(gè)新函數(shù)。以后,每次運(yùn)行這個(gè)新函數(shù),就只需要提供另一個(gè)參數(shù)就夠了。

bind方法有一些使用注意點(diǎn)。

(1)每一次返回一個(gè)新函數(shù)

bind方法每運(yùn)行一次,就返回一個(gè)新函數(shù),這會(huì)產(chǎn)生一些問題。比如,監(jiān)聽事件的時(shí)候,不能寫成下面這樣。

element.addEventListener('click', o.m.bind(o));

上面代碼表示,click事件綁定bind方法生成的一個(gè)匿名函數(shù)。這樣會(huì)導(dǎo)致無法取消綁定,所以,下面的代碼是無效的。

element.removeEventListener('click', o.m.bind(o));

正確的方法是寫成下面這樣:

var listener = o.m.bind(o);
element.addEventListener('click', listener);
//  ...
element.removeEventListener('click', listener);

(2)bind方法的自定義代碼

對(duì)于那些不支持bind方法的老式瀏覽器,可以自行定義bind方法。

if(!('bind' in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this;
        var context = arguments[0];
        var args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args);
        }
    }
}

(3)jQuery的proxy方法

除了用bind方法綁定函數(shù)運(yùn)行時(shí)所在的對(duì)象,還可以使用jQuery的$.proxy方法,它與bind方法的作用基本相同。

$("#button").on("click", $.proxy(o.f, o));

上面代碼表示,$.proxy方法將o.f方法綁定到o對(duì)象。

(4)結(jié)合call方法使用

利用bind方法,可以改寫一些JavaScript原生方法的使用形式,以數(shù)組的slice方法為例。

[1,2,3].slice(0,1) 
// [1]

// 等同于

Array.prototype.slice.call([1,2,3], 0, 1)
// [1]

上面的代碼中,數(shù)組的slice方法從[1, 2, 3]里面,按照指定位置和長度切分出另一個(gè)數(shù)組。這樣做的本質(zhì)是在[1, 2, 3]上面調(diào)用Array.prototype.slice方法,因此可以用call方法表達(dá)這個(gè)過程,得到同樣的結(jié)果。

call方法實(shí)質(zhì)上是調(diào)用Function.prototype.call方法,因此上面的表達(dá)式可以用bind方法改寫。

var slice = Function.prototype.call.bind(Array.prototype.slice);

slice([1, 2, 3], 0, 1) // [1]

可以看到,利用bind方法,將[1, 2, 3].slice(0, 1)變成了slice([1, 2, 3], 0, 1)的形式。這種形式的改變還可以用于其他數(shù)組方法。

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]

如果再進(jìn)一步,將Function.prototype.call方法綁定到Function.prototype.bind對(duì)象,就意味著bind的調(diào)用形式也可以被改寫。

function f(){
    console.log(this.v);
}

var o = { v: 123 };

var bind = Function.prototype.call.bind(Function.prototype.bind);

bind(f,o)() // 123

上面代碼表示,將Function.prototype.call方法綁定Function.prototype.bind以后,bind方法的使用形式從f.bind(o),變成了bind(f, o)。

參考鏈接

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)