Javascript技術(shù)棧中的四種依賴注入詳解
作為面向?qū)ο缶幊讨袑?shí)現(xiàn)控制反轉(zhuǎn)(Inversion of Control,下文稱IoC)最常見(jiàn)的技術(shù)手段之一,依賴注入(Dependency Injection,下文稱DI)可謂在OOP編程中大行其道經(jīng)久不衰。比如在J2EE中,就有大名鼎鼎的執(zhí)牛耳者Spring。Javascript社區(qū)中自然也不乏一些積極的嘗試,廣為人知的AngularJS很大程度上就是基于DI實(shí)現(xiàn)的。遺憾的是,作為一款缺少反射機(jī)制、不支持Annotation語(yǔ)法的動(dòng)態(tài)語(yǔ)言,Javascript長(zhǎng)期以來(lái)都沒(méi)有屬于自己的Spring框架。當(dāng)然,伴隨著ECMAScript草案進(jìn)入快速迭代期的春風(fēng),Javascript社區(qū)中的各種方言、框架可謂群雄并起,方興未艾??梢灶A(yù)見(jiàn)到,優(yōu)秀的JavascriptDI框架的出現(xiàn)只是早晚的事。
本文總結(jié)了Javascript中常見(jiàn)的依賴注入方式,并以inversify.js為例,文章分為四節(jié):
一. 基于Injector、Cache和函數(shù)參數(shù)名的依賴注入
二. AngularJS中基于雙Injector的依賴注入
三. TypeScript中基于裝飾器和反射的依賴注入
四. inversify.js——Javascript技術(shù)棧中的IoC容器
一. 基于Injector、Cache和函數(shù)參數(shù)名的依賴注入
盡管Javascript中不原生支持反射(Reflection)語(yǔ)法,但是Function.prototype上的toString方法卻為我們另辟蹊徑,使得在運(yùn)行時(shí)窺探某個(gè)函數(shù)的內(nèi)部構(gòu)造成為可能:toString方法會(huì)以字符串的形式返回包含function關(guān)鍵字在內(nèi)的整個(gè)函數(shù)定義。從這個(gè)完整的函數(shù)定義出發(fā),我們可以利用正則表達(dá)式提取出該函數(shù)所需要的參數(shù),從而在某種程度上得知該函數(shù)的運(yùn)行依賴。
比如Student類上write方法的函數(shù)簽名write(notebook, pencil)就說(shuō)明它的執(zhí)行依賴于notebook和pencil對(duì)象。因此,我們可以首先把notebook和pencil對(duì)象存放到某個(gè)cache中,再通過(guò)injector(注入器、注射器)向write方法提供它所需要的依賴:
var cache = {}; // 通過(guò)解析Function.prototype.toString()取得參數(shù)名 function getParamNames(func) { 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對(duì)象上,bind對(duì)象可以為空 resolve: function (func, bind) { // 取得參數(shù)名 var paramNames = getParamNames(func); var params = []; for (var i = 0; i < paramNames.length; i++) { // 通過(guò)參數(shù)名在cache中取出相應(yīng)的依賴 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...
有時(shí)候?yàn)榱吮WC良好的封裝性,也不一定要把cache對(duì)象暴露給外界作用域,更多的時(shí)候是以閉包變量或者私有屬性的形式存在的:
function Injector() { this._cache = {}; } Injector.prototype.put = function (name, obj) { this._cache[name] = obj; }; Injector.prototype.getParamNames = function (func) { 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類上的另一個(gè)方法function draw(notebook, pencil, eraser),因?yàn)閕njector的cache中已經(jīng)有了notebook和pencil對(duì)象,我們只需要將額外的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);
通過(guò)依賴注入,函數(shù)的執(zhí)行和其所依賴對(duì)象的創(chuàng)建邏輯就被解耦開(kāi)來(lái)了。
當(dāng)然,隨著grunt/gulp/fis等前端工程化工具的普及,越來(lái)越多的項(xiàng)目在上線之前都經(jīng)過(guò)了代碼混淆(uglify),因而通過(guò)參數(shù)名去判斷依賴并不總是可靠,有時(shí)候也會(huì)通過(guò)為function添加額外屬性的方式來(lái)明確地說(shuō)明其依賴:
Student.prototype.write.depends = ['notebook', 'pencil']; Student.prototype.draw.depends = ['notebook', 'pencil', 'eraser']; Injector.prototype.resolve = function (func, bind) { var self = this; // 首先檢查func上是否有depends屬性,如果沒(méi)有,再用正則表達(dá)式解析 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的同學(xué)很快就能聯(lián)想到,在injector注入之前,我們?cè)诙xmodule時(shí)還可以調(diào)用config方法來(lái)配置隨后會(huì)被注入的對(duì)象。典型的例子就是在使用路由時(shí)對(duì)$routeProvider的配置。也就是說(shuō),不同于上一小節(jié)中直接將現(xiàn)成對(duì)象(比如new Notebook())存入cache的做法,AngularJS中的依賴注入應(yīng)該還有一個(gè)"實(shí)例化"或者"調(diào)用工廠方法"的過(guò)程。
這就是providerInjector、instanceInjector以及他們各自所擁有的providerCache和instanceCache的由來(lái)。
在AngularJS中,我們能夠通過(guò)依賴注入獲取到的injector通常是instanceInjector,而providerInjector則是以閉包中變量的形式存在的。每當(dāng)我們需要AngularJS提供依賴注入服務(wù)時(shí),比如想要獲取notebook,instanceInjector會(huì)首先查詢instanceCache上是存在notebook屬性,如果存在,則直接注入;如果不存在,則將這個(gè)任務(wù)轉(zhuǎn)交給providerInjector;providerInjector會(huì)將"Provider"字符串拼接到"notebook"字符串的后面,組成一個(gè)新的鍵名"notebookProvider",再到providerCache中查詢是否有notebookProvider這個(gè)屬性,如有沒(méi)有,則拋出異常Unknown Provider異常:
如果有,則將這個(gè)provider返回給instanceInjector;instanceInjector拿到notebookProvider后,會(huì)調(diào)用notebookProvider上的工廠方法$get,獲取返回值notebook對(duì)象,將該對(duì)象放到instanceCache中以備將來(lái)使用,同時(shí)也注入到一開(kāi)始聲明這個(gè)依賴的函數(shù)中。過(guò)程描述起來(lái)比較復(fù)雜,可以通過(guò)下面的圖示來(lái)說(shuō)明:
需要注意的是,AngularJS中的依賴注入方式也是有缺陷的:利用一個(gè)instanceInjector單例服務(wù)全局的副作用就是無(wú)法單獨(dú)跟蹤和控制某一條依賴鏈條,即使在沒(méi)有交叉依賴的情況下,不同module中的同名provider也會(huì)產(chǎn)生覆蓋,這里就不詳細(xì)展開(kāi)了。
另外,對(duì)于習(xí)慣于Java和C#等語(yǔ)言中高級(jí)IoC容器的同學(xué)來(lái)說(shuō),看到這里可能覺(jué)得有些別扭,畢竟在OOP中,我們通常不會(huì)將依賴以參數(shù)的形式傳遞給方法,而是作為屬性通過(guò)constructor或者setters傳遞給實(shí)例,以實(shí)現(xiàn)封裝。的確如此,一、二節(jié)中的依賴注入方式?jīng)]有體現(xiàn)出足夠的面向?qū)ο筇匦?,畢竟這種方式在Javascript已經(jīng)存在多年了,甚至都不需要ES5的語(yǔ)法支持。希望了解Javascript社區(qū)中最近一兩年關(guān)于依賴注入的研究和成果的同學(xué),可以繼續(xù)往下閱讀。
三. TypeScript中基于裝飾器和反射的依賴注入
筆者本身對(duì)于Javascript的各種方言的學(xué)習(xí)并不是特別熱情,尤其是現(xiàn)在EMCAScript提案、草案更新很快,很多時(shí)候借助于polyfill和babel的各種preset就能滿足需求了。但是TypeScript是一個(gè)例外(當(dāng)然現(xiàn)在Decorator也已經(jīng)是提案了,雖然階段還比較早,但是確實(shí)已經(jīng)有polyfill可以使用)。上文提到,Javascript社區(qū)中遲遲沒(méi)有出現(xiàn)一款優(yōu)秀的IoC容器和自身的語(yǔ)言特性有關(guān),那就依賴注入這個(gè)話題而言,TypeScript給我們帶來(lái)了什么不同呢?至少有下面這幾點(diǎn):
* TypeScript增加了編譯時(shí)類型檢查,使Javascript具備了一定的靜態(tài)語(yǔ)言特性
* TypeScript支持裝飾器(Decorator)語(yǔ)法,和傳統(tǒng)的注解(Annotation)頗為相似
* TypeScript支持元信息(Metadata)反射,不再需要調(diào)用Function.prototype.toString方法
下面我們就嘗試?yán)肨ypeScript帶來(lái)的新語(yǔ)法來(lái)規(guī)范和簡(jiǎn)化依賴注入。這次我們不再向函數(shù)或方法中注入依賴了,而是向類的構(gòu)造函數(shù)中注入。
TypeScript支持對(duì)類、方法、屬性和函數(shù)參數(shù)進(jìn)行裝飾,這里需要用到的是對(duì)類的裝飾。繼續(xù)上面小節(jié)中用到的例子,利用TypeScript對(duì)代碼進(jìn)行一些重構(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的實(shí)現(xiàn)。injector的resolve方法在接收到傳入的構(gòu)造函數(shù)時(shí),會(huì)通過(guò)name屬性取出該構(gòu)造函數(shù)的名字,比如class Student,它的name屬性就是字符串"Student"。再將Student作為key,到dependenciesMap中去取出Student的依賴,至于dependenciesMap中是何時(shí)存入的依賴關(guān)系,這是裝飾器Inject的邏輯,后面會(huì)談到。Student的依賴取出后,由于這些依賴已經(jīng)是構(gòu)造函數(shù)的引用而非簡(jiǎn)單的字符串了(比如Notebook、Pencil的構(gòu)造函數(shù)),因此直接使用new語(yǔ)句即可獲取這些對(duì)象。獲取到Student類所依賴的對(duì)象之后,如何把這些依賴作為構(gòu)造函數(shù)的參數(shù)傳入到Student中呢?最簡(jiǎn)單的莫過(guò)于ES6的spread操作符。在不能使用ES6的環(huán)境下,我們也可以通過(guò)偽造一個(gè)構(gòu)造函數(shù)來(lái)完成上述邏輯。注意為了使instanceof操作符不失效,這個(gè)偽造的構(gòu)造函數(shù)的prototype屬性應(yīng)該指向原構(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的語(yǔ)法,下面的代碼可以合并為一行: // 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的邏輯完成后,就可以用來(lái)裝飾class Student并享受依賴注入帶來(lái)的樂(lè)趣了:
// 裝飾器的使用非常簡(jiǎn)單,只需要在類定義的上方添加一行代碼 // 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
利用裝飾器,我們還可以實(shí)現(xiàn)一種比較激進(jìn)的依賴注入,下文稱之為RadicalInject。RadicalInject對(duì)原代碼的侵入性比較強(qiáng),不一定適合具體的業(yè)務(wù),這里也一并介紹一下。要理解RadicalInject,需要對(duì)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呢?因?yàn)槭褂肦adicalInject對(duì)Student方法裝飾之后, // Student指向的構(gòu)造函數(shù)已經(jīng)不是一開(kāi)始我們聲明的class Student了,而是這里的返回值, // 即reservedConstructor。Student的指向變了并不是一件不能接受的事,但是如果要 // 保證student instanceof Student如我們所期望的那樣工作,這里就應(yīng)該將 // reservedConstructor的prototype屬性指向原Student的prototype function reservedConstructor() { return new mockConstructor(); } reservedConstructor.prototype = target.prototype; return reservedConstructor; } return wrappedFunc; }
使用RadicalInject,原構(gòu)造函數(shù)實(shí)質(zhì)上已經(jīng)被一個(gè)新的函數(shù)代理了,使用上也更為簡(jiǎn)單,甚至都不需要再有injector的實(shí)現(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方法需要接收三個(gè)參數(shù),直接無(wú)參調(diào)用new Student()會(huì)造成TypeScript編譯器報(bào)錯(cuò)。當(dāng)然這里只是分享一種思路,大家可以暫時(shí)忽略這個(gè)錯(cuò)誤。有興趣的同學(xué)也可以使用類似的思路嘗試代理一個(gè)工廠方法,而非直接代理構(gòu)造函數(shù),以避免這類錯(cuò)誤,這里不再展開(kāi)。
AngularJS2團(tuán)隊(duì)為了獲得更好的裝飾器和反射語(yǔ)法的支持,一度準(zhǔn)備另起爐灶,基于AtScript(AtScript中的"A"指的就是Annotation)來(lái)進(jìn)行新框架的開(kāi)發(fā)。但最終卻選擇擁抱TypeScript,于是便有了微軟和谷歌的奇妙組合。
當(dāng)然,需要說(shuō)明的是,在缺少相關(guān)標(biāo)準(zhǔn)和瀏覽器廠商支持的情況下,TypeScript在運(yùn)行時(shí)只是純粹的Javascript,下節(jié)中出現(xiàn)的例子會(huì)印證這一點(diǎn)。
四. inversify.js——Javascript技術(shù)棧中的IoC容器
其實(shí)從Javascript出現(xiàn)各種支持高級(jí)語(yǔ)言特性的方言就可以預(yù)見(jiàn)到,IoC容器的出現(xiàn)只是早晚的事情。比如博主今天要介紹的基于TypeScript的inversify.js,就是其中的先行者之一。
inversity.js比上節(jié)中博主實(shí)現(xiàn)的例子還要進(jìn)步很多,它最初設(shè)計(jì)的目的就是為了前端工程師同學(xué)們能在Javascript中寫(xiě)出符合SOLID原則的代碼,立意可謂非常之高。表現(xiàn)在代碼中,就是處處有接口,將"Depend upon Abstractions. Do not depend upon concretions."(依賴于抽象,而非依賴于具體)表現(xiàn)地淋漓盡致。繼續(xù)使用上面的例子,但是由于inversity.js是面向接口的,上面的代碼需要進(jìn)一步重構(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框架,這次我們就不用自己實(shí)現(xiàn)injector和Inject裝飾器啦,只需要從inversify模塊中引用相關(guān)對(duì)象:
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中各種概念只是語(yǔ)法糖嗎?不同于上一節(jié)中直接將constructor引用傳遞給Inject的例子,由于inversify.js是面向接口的,而諸如NotebookInterface、PencilInterface之類的接口只是由TypeScript提供的語(yǔ)法糖,在運(yùn)行時(shí)并不存在,因此我們?cè)谘b飾器中聲明依賴時(shí)只能使用字符串形式而非引用形式。不過(guò)不用擔(dān)心,inversify.js為我們提供了bind機(jī)制,在接口的字符串形式和具體的構(gòu)造函數(shù)之間搭建了橋梁:
import { TypeBinding, Kernel } from "inversify"; var kernel = new Kernel(); kernel.bind(new TypeBinding<NotebookInterface>("NotebookInterface", Notebook)); kernel.bind(new TypeBinding<PencilInterface>("PencilInterface", Pencil)); kernel.bind(new TypeBinding<EraserInterface>("EraserInterface", Eraser)); kernel.bind(new TypeBinding<StudentInterface>("StudentInterface", Student));
注意這步需要從inversify模塊中引入TypeBinding和Kernel,并且為了保證返回值類型以及整個(gè)編譯時(shí)靜態(tài)類型檢查能夠順利通過(guò),泛型語(yǔ)法也被使用了起來(lái)。
說(shuō)到這里,要理解new TypeBinding<NotebookInterface>("NotebookInterface", Notebook)也就很自然了:為依賴于"NotebookInterface"字符串的類提供Notebook類的實(shí)例,返回值向上溯型到NotebookInterface。
完成了這些步驟,使用起來(lái)也還算順手:
var student: StudentInterface = kernel.resolve<StudentInterface>("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
以上就是關(guān)于Javascript技術(shù)棧中的四種依賴注入的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助。
相關(guān)文章
JS正則表達(dá)式判斷有效數(shù)實(shí)例代碼
這篇文章主要介紹了JS正則表達(dá)式判斷有效數(shù)實(shí)例代碼,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03JS小功能(setInterval實(shí)現(xiàn)圖片效果顯示時(shí)間)實(shí)例代碼
這篇文章主要介紹了setInterval實(shí)現(xiàn)圖片效果顯示時(shí)間實(shí)例代碼,有需要的朋友可以參考一下2013-11-11JavaScript手寫(xiě)數(shù)組的常用函數(shù)總結(jié)
這篇文章主要給大家介紹了關(guān)于JavaScript手寫(xiě)數(shù)組常用函數(shù)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Mozilla 表達(dá)式 __noSuchMethod__
這是一個(gè)很特殊的方法,但是其存在的意義很大。不過(guò)很可惜只有firefox支持了。一個(gè)簡(jiǎn)單的例子解釋一下它的用處2009-04-04Antd中Table列表行默認(rèn)包含修改及刪除功能的封裝方法
這篇文章主要介紹了Antd中Table列表行默認(rèn)包含修改及刪除功能的封裝,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12