欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Javascript技術(shù)棧中的四種依賴注入小結(jié)

 更新時間:2016年02月27日 15:46:21   投稿:hebedich  
本文總結(jié)了Javascript中常見的依賴注入方式,并以inversify.js為例,介紹了方言社區(qū)對于Javascript中DI框架的嘗試和初步成果

作為面向?qū)ο缶幊讨袑崿F(xiàn)控制反轉(zhuǎn)(Inversion of Control,下文稱IoC)最常見的技術(shù)手段之一,依賴注入(Dependency Injection,下文稱DI)可謂在OOP編程中大行其道經(jīng)久不衰。比如在J2EE中,就有大名鼎鼎的執(zhí)牛耳者Spring。Javascript社區(qū)中自然也不乏一些積極的嘗試,廣為人知的AngularJS很大程度上就是基于DI實現(xiàn)的。遺憾的是,作為一款缺少反射機制、不支持Annotation語法的動態(tài)語言,Javascript長期以來都沒有屬于自己的Spring框架。當然,伴隨著ECMAScript草案進入快速迭代期的春風,Javascript社區(qū)中的各種方言、框架可謂群雄并起,方興未艾??梢灶A見到,優(yōu)秀的JavascriptDI框架的出現(xiàn)只是早晚的事。

本文總結(jié)了Javascript中常見的依賴注入方式,并以inversify.js為例,介紹了方言社區(qū)對于Javascript中DI框架的嘗試和初步成果。文章分為四節(jié):

一. 基于Injector、Cache和函數(shù)參數(shù)名的依賴注入
二. AngularJS中基于雙Injector的依賴注入
三. TypeScript中基于裝飾器和反射的依賴注入
四. inversify.js——Javascript技術(shù)棧中的IoC容器

一. 基于Injector、Cache和函數(shù)參數(shù)名的依賴注入

盡管Javascript中不原生支持反射(Reflection)語法,但是Function.prototype上的toString方法卻為我們另辟蹊徑,使得在運行時窺探某個函數(shù)的內(nèi)部構(gòu)造成為可能:toString方法會以字符串的形式返回包含function關(guān)鍵字在內(nèi)的整個函數(shù)定義。從這個完整的函數(shù)定義出發(fā),我們可以利用正則表達式提取出該函數(shù)所需要的參數(shù),從而在某種程度上得知該函數(shù)的運行依賴。
比如Student類上write方法的函數(shù)簽名write(notebook, pencil)就說明它的執(zhí)行依賴于notebook和pencil對象。因此,我們可以首先把notebook和pencil對象存放到某個cache中,再通過injector(注入器、注射器)向write方法提供它所需要的依賴:

var cache = {};
// 通過解析Function.prototype.toString()取得參數(shù)名
function getParamNames(func) {
  // 正則表達式出自http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript
  var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1];
  paramNames = paramNames.replace(/ /g, '');
  paramNames = paramNames.split(',');
  return paramNames;
}
var injector = {
  // 將func作用域中的this關(guān)鍵字綁定到bind對象上,bind對象可以為空
  resolve: function (func, bind) {
    // 取得參數(shù)名
    var paramNames = getParamNames(func);
    var params = [];
    for (var i = 0; i < paramNames.length; i++) {
      // 通過參數(shù)名在cache中取出相應的依賴
      params.push(cache[paramNames[i]]);
    }
    // 注入依賴并執(zhí)行函數(shù)
    func.apply(bind, params);
  }
};
 
function Notebook() {}
Notebook.prototype.printName = function () {
  console.log('this is a notebook');
};
 
function Pencil() {}
Pencil.prototype.printName = function () {
  console.log('this is a pencil');
};
 
function Student() {}
Student.prototype.write = function (notebook, pencil) {
  if (!notebook || !pencil) {
    throw new Error('Dependencies not provided!');
  }
  console.log('writing...');
};
// 提供notebook依賴
cache['notebook'] = new Notebook();
// 提供pencil依賴
cache['pencil'] = new Pencil();
var student = new Student();
injector.resolve(student.write, student); // writing...

有時候為了保證良好的封裝性,也不一定要把cache對象暴露給外界作用域,更多的時候是以閉包變量或者私有屬性的形式存在的:

function Injector() {
  this._cache = {};
}
 
Injector.prototype.put = function (name, obj) {
  this._cache[name] = obj;
};
 
Injector.prototype.getParamNames = function (func) {
  // 正則表達式出自http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript
  var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1];
  paramNames = paramNames.replace(/ /g, '');
  paramNames = paramNames.split(',');
  return paramNames;
};
 
Injector.prototype.resolve = function (func, bind) {
  var self = this;
  var paramNames = self.getParamNames(func);
  var params = paramNames.map(function (name) {
    return self._cache[name];
  });
  func.apply(bind, params);
};
 
var injector = new Injector();
 
var student = new Student();
injector.put('notebook', new Notebook());
injector.put('pencil', new Pencil())
injector.resolve(student.write, student); // writing...

比如現(xiàn)在要執(zhí)行Student類上的另一個方法function draw(notebook, pencil, eraser),因為injector的cache中已經(jīng)有了notebook和pencil對象,我們只需要將額外的eraser也存放到cache中:

function Eraser() {}
Eraser.prototype.printName = function () {
  console.log('this is an eraser');
};
 
// 為Student增加draw方法
Student.prototype.draw = function (notebook, pencil, eraser) {
  if (!notebook || !pencil || !eraser) {
    throw new Error('Dependencies not provided!');
  }
  console.log('drawing...');
};
 
injector.put('eraser', new Eraser());
injector.resolve(student.draw, student);

通過依賴注入,函數(shù)的執(zhí)行和其所依賴對象的創(chuàng)建邏輯就被解耦開來了。
當然,隨著grunt/gulp/fis等前端工程化工具的普及,越來越多的項目在上線之前都經(jīng)過了代碼混淆(uglify),因而通過參數(shù)名去判斷依賴并不總是可靠,有時候也會通過為function添加額外屬性的方式來明確地說明其依賴:

Student.prototype.write.depends = ['notebook', 'pencil'];
Student.prototype.draw.depends = ['notebook', 'pencil', 'eraser'];
Injector.prototype.resolve = function (func, bind) {
  var self = this;
  // 首先檢查func上是否有depends屬性,如果沒有,再用正則表達式解析
  func.depends = func.depends || self.getParamNames(func);
  var params = func.depends.map(function (name) {
    return self._cache[name];
  });
  func.apply(bind, params);
};
var student = new Student();
injector.resolve(student.write, student); // writing...
injector.resolve(student.draw, student); // draw...

二. AngularJS中基于雙Injector的依賴注入

熟悉AngularJS的同學很快就能聯(lián)想到,在injector注入之前,我們在定義module時還可以調(diào)用config方法來配置隨后會被注入的對象。典型的例子就是在使用路由時對$routeProvider的配置。也就是說,不同于上一小節(jié)中直接將現(xiàn)成對象(比如new Notebook())存入cache的做法,AngularJS中的依賴注入應該還有一個”實例化”或者”調(diào)用工廠方法”的過程。
這就是providerInjector、instanceInjector以及他們各自所擁有的providerCache和instanceCache的由來。
在AngularJS中,我們能夠通過依賴注入獲取到的injector通常是instanceInjector,而providerInjector則是以閉包中變量的形式存在的。每當我們需要AngularJS提供依賴注入服務時,比如想要獲取notebook,instanceInjector會首先查詢instanceCache上是存在notebook屬性,如果存在,則直接注入;如果不存在,則將這個任務轉(zhuǎn)交給providerInjector;providerInjector會將”Provider”字符串拼接到”notebook”字符串的后面,組成一個新的鍵名”notebookProvider”,再到providerCache中查詢是否有notebookProvider這個屬性,如有沒有,則拋出異常Unknown Provider異常:

如果有,則將這個provider返回給instanceInjector;instanceInjector拿到notebookProvider后,會調(diào)用notebookProvider上的工廠方法$get,獲取返回值notebook對象,將該對象放到instanceCache中以備將來使用,同時也注入到一開始聲明這個依賴的函數(shù)中。

需要注意的是,AngularJS中的依賴注入方式也是有缺陷的:利用一個instanceInjector單例服務全局的副作用就是無法單獨跟蹤和控制某一條依賴鏈條,即使在沒有交叉依賴的情況下,不同module中的同名provider也會產(chǎn)生覆蓋,這里就不詳細展開了。

另外,對于習慣于Java和C#等語言中高級IoC容器的同學來說,看到這里可能覺得有些別扭,畢竟在OOP中,我們通常不會將依賴以參數(shù)的形式傳遞給方法,而是作為屬性通過constructor或者setters傳遞給實例,以實現(xiàn)封裝。的確如此,一、二節(jié)中的依賴注入方式?jīng)]有體現(xiàn)出足夠的面向?qū)ο筇匦?,畢竟這種方式在Javascript已經(jīng)存在多年了,甚至都不需要ES5的語法支持。希望了解Javascript社區(qū)中最近一兩年關(guān)于依賴注入的研究和成果的同學,可以繼續(xù)往下閱讀。

三. TypeScript中基于裝飾器和反射的依賴注入

博主本身對于Javascript的各種方言的學習并不是特別熱情,尤其是現(xiàn)在EMCAScript提案、草案更新很快,很多時候借助于polyfill和babel的各種preset就能滿足需求了。但是TypeScript是一個例外(當然現(xiàn)在Decorator也已經(jīng)是提案了,雖然階段還比較早,但是確實已經(jīng)有polyfill可以使用)。上文提到,Javascript社區(qū)中遲遲沒有出現(xiàn)一款優(yōu)秀的IoC容器和自身的語言特性有關(guān),那就依賴注入這個話題而言,TypeScript給我們帶來了什么不同呢?至少有下面這幾點:
* TypeScript增加了編譯時類型檢查,使Javascript具備了一定的靜態(tài)語言特性
* TypeScript支持裝飾器(Decorator)語法,和傳統(tǒng)的注解(Annotation)頗為相似
* TypeScript支持元信息(Metadata)反射,不再需要調(diào)用Function.prototype.toString方法
下面我們就嘗試利用TypeScript帶來的新語法來規(guī)范和簡化依賴注入。這次我們不再向函數(shù)或方法中注入依賴了,而是向類的構(gòu)造函數(shù)中注入。
TypeScript支持對類、方法、屬性和函數(shù)參數(shù)進行裝飾,這里需要用到的是對類的裝飾。繼續(xù)上面小節(jié)中用到的例子,利用TypeScript對代碼進行一些重構(gòu):

class Pencil {
  public printName() {
    console.log('this is a pencil');
  }
}
 
class Eraser {
  public printName() {
    console.log('this is an eraser');
  }
}
 
class Notebook {
  public printName() {
    console.log('this is a notebook');
  }
}
 
class Student {
  pencil: Pencil;
  eraser: Eraser;
  notebook: Notebook;
  public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) {
    this.notebook = notebook;
    this.pencil = pencil;
    this.eraser = eraser;
  }
  public write() {
    if (!this.notebook || !this.pencil) {
      throw new Error('Dependencies not provided!');
    }
    console.log('writing...');
  }
  public draw() {
    if (!this.notebook || !this.pencil || !this.eraser) {
      throw new Error('Dependencies not provided!');
    }
    console.log('drawing...');
  }
}

下面是injector和裝飾器Inject的實現(xiàn)。injector的resolve方法在接收到傳入的構(gòu)造函數(shù)時,會通過name屬性取出該構(gòu)造函數(shù)的名字,比如class Student,它的name屬性就是字符串”Student”。再將Student作為key,到dependenciesMap中去取出Student的依賴,至于dependenciesMap中是何時存入的依賴關(guān)系,這是裝飾器Inject的邏輯,后面會談到。Student的依賴取出后,由于這些依賴已經(jīng)是構(gòu)造函數(shù)的引用而非簡單的字符串了(比如Notebook、Pencil的構(gòu)造函數(shù)),因此直接使用new語句即可獲取這些對象。獲取到Student類所依賴的對象之后,如何把這些依賴作為構(gòu)造函數(shù)的參數(shù)傳入到Student中呢?最簡單的莫過于ES6的spread操作符。在不能使用ES6的環(huán)境下,我們也可以通過偽造一個構(gòu)造函數(shù)來完成上述邏輯。注意為了使instanceof操作符不失效,這個偽造的構(gòu)造函數(shù)的prototype屬性應該指向原構(gòu)造函數(shù)的prototype屬性。

var dependenciesMap = {};
var injector = {
  resolve: function (constructor) {
    var dependencies = dependenciesMap[constructor.name];
    dependencies = dependencies.map(function (dependency) {
      return new dependency();
    });
    // 如果可以使用ES6的語法,下面的代碼可以合并為一行:
    // return new constructor(...dependencies);
    var mockConstructor: any = function () {
      constructor.apply(this, dependencies);
    };
    mockConstructor.prototype = constructor.prototype;
    return new mockConstructor();
  }
};
function Inject(...dependencies) {
  return function (constructor) {
    dependenciesMap[constructor.name] = dependencies;
    return constructor;
  };
}

injector和裝飾器Inject的邏輯完成后,就可以用來裝飾class Student并享受依賴注入帶來的樂趣了:

// 裝飾器的使用非常簡單,只需要在類定義的上方添加一行代碼
// Inject是裝飾器的名字,后面是function Inject的參數(shù)
@Inject(Notebook, Pencil, Eraser)
class Student {
  pencil: Pencil;
  eraser: Eraser;
  notebook: Notebook;
  public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) {
    this.notebook = notebook;
    this.pencil = pencil;
    this.eraser = eraser;
  }
  public write() {
    if (!this.notebook || !this.pencil) {
      throw new Error('Dependencies not provided!');
    }
    console.log('writing...');
  }
  public draw() {
    if (!this.notebook || !this.pencil || !this.eraser) {
      throw new Error('Dependencies not provided!');
    }
    console.log('drawing...');
  }
}
var student = injector.resolve(Student);
console.log(student instanceof Student); // true
student.notebook.printName(); // this is a notebook
student.pencil.printName(); // this is a pencil
student.eraser.printName(); // this is an eraser
student.draw(); // drawing
student.write(); // writing

利用裝飾器,我們還可以實現(xiàn)一種比較激進的依賴注入,下文稱之為RadicalInject。RadicalInject對原代碼的侵入性比較強,不一定適合具體的業(yè)務,這里也一并介紹一下。要理解RadicalInject,需要對TypeScript裝飾器的原理和Array.prototype上的reduce方法理解比較到位。

function RadicalInject(...dependencies){
  var wrappedFunc:any = function (target: any) {
    dependencies = dependencies.map(function (dependency) {
      return new dependency();
    });
    // 使用mockConstructor的原因和上例相同
    function mockConstructor() {
      target.apply(this, dependencies);
    }
    mockConstructor.prototype = target.prototype;
 
    // 為什么需要使用reservedConstructor呢?因為使用RadicalInject對Student方法裝飾之后,
    // Student指向的構(gòu)造函數(shù)已經(jīng)不是一開始我們聲明的class Student了,而是這里的返回值,
    // 即reservedConstructor。Student的指向變了并不是一件不能接受的事,但是如果要
    // 保證student instanceof Student如我們所期望的那樣工作,這里就應該將
    // reservedConstructor的prototype屬性指向原Student的prototype
    function reservedConstructor() {
      return new mockConstructor();
    }
    reservedConstructor.prototype = target.prototype;
    return reservedConstructor;
  }
  return wrappedFunc;
}

使用RadicalInject,原構(gòu)造函數(shù)實質(zhì)上已經(jīng)被一個新的函數(shù)代理了,使用上也更為簡單,甚至都不需要再有injector的實現(xiàn):

@RadicalInject(Notebook, Pencil, Eraser)
class Student {
  pencil: Pencil;
  eraser: Eraser;
  notebook: Notebook;
  public constructor() {}
  public constructor(notebook: Notebook, pencil: Pencil, eraser: Eraser) {
    this.notebook = notebook;
    this.pencil = pencil;
    this.eraser = eraser;
  }
  public write() {
    if (!this.notebook || !this.pencil) {
      throw new Error('Dependencies not provided!');
    }
    console.log('writing...');
  }
  public draw() {
    if (!this.notebook || !this.pencil || !this.eraser) {
      throw new Error('Dependencies not provided!');
    }
    console.log('drawing...');
  }
}
// 不再出現(xiàn)injector,直接調(diào)用構(gòu)造函數(shù)
var student = new Student(); 
console.log(student instanceof Student); // true
student.notebook.printName(); // this is a notebook
student.pencil.printName(); // this is a pencil
student.eraser.printName(); // this is an eraser
student.draw(); // drawing
student.write(); // writing

由于class Student的constructor方法需要接收三個參數(shù),直接無參調(diào)用new Student()會造成TypeScript編譯器報錯。當然這里只是分享一種思路,大家可以暫時忽略這個錯誤。有興趣的同學也可以使用類似的思路嘗試代理一個工廠方法,而非直接代理構(gòu)造函數(shù),以避免這類錯誤,這里不再展開。

AngularJS2團隊為了獲得更好的裝飾器和反射語法的支持,一度準備另起爐灶,基于AtScript(AtScript中的”A”指的就是Annotation)來進行新框架的開發(fā)。但最終卻選擇擁抱TypeScript,于是便有了微軟和谷歌的奇妙組合。

當然,需要說明的是,在缺少相關(guān)標準和瀏覽器廠商支持的情況下,TypeScript在運行時只是純粹的Javascript,下節(jié)中出現(xiàn)的例子會印證這一點。

四. inversify.js——Javascript技術(shù)棧中的IoC容器

其實從Javascript出現(xiàn)各種支持高級語言特性的方言就可以預見到,IoC容器的出現(xiàn)只是早晚的事情。比如博主今天要介紹的基于TypeScript的inversify.js,就是其中的先行者之一。
inversity.js比上節(jié)中博主實現(xiàn)的例子還要進步很多,它最初設計的目的就是為了前端工程師同學們能在Javascript中寫出符合SOLID原則的代碼,立意可謂非常之高。表現(xiàn)在代碼中,就是處處有接口,將”Depend upon Abstractions. Do not depend upon concretions.”(依賴于抽象,而非依賴于具體)表現(xiàn)地淋漓盡致。繼續(xù)使用上面的例子,但是由于inversity.js是面向接口的,上面的代碼需要進一步重構(gòu):

interface NotebookInterface {
  printName(): void;
}
interface PencilInterface {
  printName(): void;
}
interface EraserInterface {
  printName(): void;
}
interface StudentInterface {
  notebook: NotebookInterface;
  pencil: PencilInterface;
  eraser: EraserInterface;
  write(): void;
  draw(): void;
}
class Notebook implements NotebookInterface {
  public printName() {
    console.log('this is a notebook');
  }
}
class Pencil implements PencilInterface {
  public printName() {
    console.log('this is a pencil');
  }
}
class Eraser implements EraserInterface {
  public printName() {
    console.log('this is an eraser');
  }
}
 
class Student implements StudentInterface {
  notebook: NotebookInterface;
  pencil: PencilInterface;
  eraser: EraserInterface;
  constructor(notebook: NotebookInterface, pencil: PencilInterface, eraser: EraserInterface) {
    this.notebook = notebook;
    this.pencil = pencil;
    this.eraser = eraser;
  }
  write() {
    console.log('writing...');
  }
  draw() {
    console.log('drawing...');
  }
}

由于使用了inversity框架,這次我們就不用自己實現(xiàn)injector和Inject裝飾器啦,只需要從inversify模塊中引用相關(guān)對象:

import { Inject } from "inversify";
 
@Inject("NotebookInterface", "PencilInterface", "EraserInterface")
class Student implements StudentInterface {
  notebook: NotebookInterface;
  pencil: PencilInterface;
  eraser: EraserInterface;
  constructor(notebook: NotebookInterface, pencil: PencilInterface, eraser: EraserInterface) {
    this.notebook = notebook;
    this.pencil = pencil;
    this.eraser = eraser;
  }
  write() {
    console.log('writing...');
  }
  draw() {
    console.log('drawing...');
  }
}

這樣就行了嗎?還記得上節(jié)中提到TypeScript中各種概念只是語法糖嗎?不同于上一節(jié)中直接將constructor引用傳遞給Inject的例子,由于inversify.js是面向接口的,而諸如NotebookInterface、PencilInterface之類的接口只是由TypeScript提供的語法糖,在運行時并不存在,因此我們在裝飾器中聲明依賴時只能使用字符串形式而非引用形式。不過不用擔心,inversify.js為我們提供了bind機制,在接口的字符串形式和具體的構(gòu)造函數(shù)之間搭建了橋梁:

import { TypeBinding, Kernel } from "inversify";
 
var kernel = new Kernel();
kernel.bind(new TypeBinding("NotebookInterface", Notebook));
kernel.bind(new TypeBinding("PencilInterface", Pencil));
kernel.bind(new TypeBinding("EraserInterface", Eraser));
kernel.bind(new TypeBinding("StudentInterface", Student));

注意這步需要從inversify模塊中引入TypeBinding和Kernel,并且為了保證返回值類型以及整個編譯時靜態(tài)類型檢查能夠順利通過,泛型語法也被使用了起來。
說到這里,要理解new TypeBinding(“NotebookInterface”, Notebook)也就很自然了:為依賴于”NotebookInterface”字符串的類提供Notebook類的實例,返回值向上溯型到NotebookInterface。
完成了這些步驟,使用起來也還算順手:

var student: StudentInterface = kernel.resolve("StudentInterface");
console.log(student instanceof Student); // true
student.notebook.printName(); // this is a notebook
student.pencil.printName(); // this is a pencil
student.eraser.printName(); // this is an eraser
student.draw(); // drawing
student.write(); // writing

最后,順帶提一下ECMAScript中相關(guān)提案的現(xiàn)狀和進展。Google的AtScript團隊曾經(jīng)有過Annotation的提案,但是AtScript胎死腹中,這個提案自然不了了之了。目前比較有希望成為es7標準的是一個關(guān)于裝飾器的提案:https://github.com/wycats/javascript-decorators。感興趣的同學可以到相關(guān)的github頁面跟蹤了解。盡管DI只是OOP編程眾多模式和特性中的一個,但卻可以折射出Javascript在OOP上艱難前進的道路。但總得說來,也算得上是路途坦蕩,前途光明?;氐揭蕾囎⑷氲脑掝}上,一邊是翹首以盼的Javascript社區(qū),一邊是姍姍來遲的IoC容器,這二者最終能產(chǎn)生怎樣的化學反應,讓我們拭目以待。

相關(guān)文章

  • document.all與WEB標準

    document.all與WEB標準

    document.all與WEB標準...
    2006-11-11
  • 在JavaScript中使用對數(shù)Math.log()方法的教程

    在JavaScript中使用對數(shù)Math.log()方法的教程

    這篇文章主要介紹了在JavaScript中使用對數(shù)Math.log()方法的教程,是JS入門學習中的基礎(chǔ)知識,需要的朋友可以參考下
    2015-06-06
  • javascript判斷office版本示例

    javascript判斷office版本示例

    這篇文章主要介紹了javascript判斷office版本示例,需要的朋友可以參考下
    2014-04-04
  • JavaScript創(chuàng)建、讀取和刪除cookie

    JavaScript創(chuàng)建、讀取和刪除cookie

    通過本文你將粗略的明白cookie是什么,如何通過js創(chuàng)建/存儲以及獲取cookie,如何讓cookie過期來刪除cookie
    2019-09-09
  • javascript 學習筆記(一)DOM基本操作

    javascript 學習筆記(一)DOM基本操作

    主要是為了使自己更加熟練操作DOM,記錄自己的點滴,規(guī)范自己的代碼!希望大家共同進步!
    2011-04-04
  • javaScript 事件綁定、事件冒泡、事件捕獲和事件執(zhí)行順序整理總結(jié)

    javaScript 事件綁定、事件冒泡、事件捕獲和事件執(zhí)行順序整理總結(jié)

    這篇文章主要介紹了javaScript 事件綁定、事件冒泡、事件捕獲和事件執(zhí)行順序整理總結(jié)的相關(guān)資料,需要的朋友可以參考下
    2016-10-10
  • javascript之Array 數(shù)組對象詳解

    javascript之Array 數(shù)組對象詳解

    本文主要是對javascript之Array 數(shù)組對象的詳解 ,比較詳細,希望能給大家做一個參考。
    2016-06-06
  • Js中的onblur和onfocus事件應用介紹

    Js中的onblur和onfocus事件應用介紹

    html頁面中,諸如按鈕、文本框等可視元素都具有擁有和失去焦點的事件,本文以文本框獲得和失去焦點為例簡單講解onfocus和onblur的應用
    2013-08-08
  • 最新評論