roxy 對象(Proxy)是 ES6 的一個非常酷卻鮮為人知的特性。雖然這個特性存在已久,但是我還是想在本文中對其稍作解釋,并用一個例子說明一下它的用法。
什么是 Proxy
正如 MDN 上簡單而枯燥的定義:
Proxy 對象用于定義基本操作的自定義行為(如屬性查找,賦值,枚舉,函數調用等)。
雖然這是一個不錯的總結,但是我卻并沒有從中搞清楚 Proxy 能做什么,以及它能幫我們實現什么。
首先,Proxy 的概念來源于元編程。簡單的說,元編程是允許我們運行我們編寫的應用程序(或核心)代碼的代碼。例如,臭名昭著的 eval 函數允許我們將字符串代碼當做可執行代碼來執行,它是就屬于元編程領域。
Proxy API 允許我們在對象和其消費實體中創建中間層,這種特性為我們提供了控制該對象的能力,比如可以決定怎樣去進行它的 get 和 set,甚至可以自定義當訪問這個對象上不存在的屬性的時候我們可以做些什么。
Proxy 的 API
var p=newProxy(target, handler);
Proxy 構造函數獲取一個 target 對象,和一個用來攔截 target 對象不同行為的 handler 對象。你可以設置下面這些攔截項:
這只是一部分攔截項,你可以在 MDN 上找到完整的列表。
下面是將 Proxy 用在驗證上的一個簡單的例子:
constCar={ maker: 'BMW', year: 2018,}; const proxyCar=newProxy(Car, {set(obj, prop, value) {if(prop==='maker'&& value.length < 1) {thrownewError('Invalid maker');} if(prop==='year'&& typeof value !=='number') {thrownewError('Invalid year');} obj[prop]=value;returntrue;} }); proxyCar.maker=''; // throw exceptionproxyCar.year='1999'; // throw exception
可以看到,我們可以用 Proxy 來驗證賦給被代理對象的值。
使用 Proxy 來調試
為了在實踐中展示 Proxy 的能力,我創建了一個簡單的監測庫,用來監測給定的對象或類,監測項如下:
這是通過在訪問任意對象、類、甚至是函數時,調用一個名為 proxyTrack 的函數來完成的。
如果你希望監測是誰給一個對象的屬性賦的值,或者一個函數執行了多久、執行了多少次、誰執行的,這個庫將非常有用。我知道可能還有其他更好的工具來實現上面的功能,但是在這里我創建這個庫就是為了用一用這個 API。
使用 proxyTrack
首先,我們看看怎么用:
functionMyClass() {} MyClass.prototype={ isPrime: function() {const num=this.num;for(var i=2; i < num; i++)if(num % i===0) returnfalse;return num !==1&& num !==0;}, num: null,}; MyClass.prototype.constructor=MyClass; const trackedClass=proxyTrack(MyClass); function start() {const my=new trackedClass(); my.num=573723653;if(!my.isPrime()) {return`${my.num} is not prime`;}} function main() { start();} main();
如果我們運行這段代碼,控制臺將會輸出:
MyClass.num is being set by start for the 1 timeMyClass.num is being get by isPrime for the 1 timeMyClass.isPrime was called by start for the 1 time and took 0 mils.MyClass.num is being get by start for the 2 time
proxyTrack 接受 2 個參數:第一個是要監測的對象/類,第二個是一個配置項對象,如果沒傳遞的話將被置為默認值。我們看看這個配置項默認值長啥樣:
const defaultOptions={ trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, stdout: null, filter: null,};
可以看到,你可以通過配置你關心的監測項來監測你的目標。比如你希望將結果輸出出來,那么你可以將 console.log 賦給 stdout。
還可以通過賦給 filter 的回調函數來自定義地控制輸出哪些信息。你將會得到一個包括有監測信息的對象,并且如果你希望保留這個信息就返回 true,反之返回 false。
在 React 中使用 proxyTrack
因為 React 的組件實際上也是類,所以你可以通過 proxyTrack 來實時監控它。比如:
classMyComponent extends Component{...} exportdefault connect(mapStateToProps)(proxyTrack(MyComponent, { trackFunctions: true, trackProps: true, trackTime: true, trackCaller: true, trackCount: true, filter: (data)=> {if( data.type==='get'&& data.prop==='componentDidUpdate') returnfalse;returntrue;}}));
可以看到,你可以將你不關心的信息過濾掉,否則輸出將會變得雜亂無章。
實現 proxyTrack
我們來看看 proxyTrack 的實現。
首先是這個函數本身:
exportfunction proxyTrack(entity, options=defaultOptions) {if(typeof entity==='function') return trackClass(entity, options);return trackObject(entity, options);}
沒什么特別的嘛,這里只是調用相關函數。
再看看 trackObject:
function trackObject(obj, options={}) {const{ trackFunctions, trackProps }=options; let resultObj=obj;if(trackFunctions) { proxyFunctions(resultObj, options);}if(trackProps) { resultObj=newProxy(resultObj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}return resultObj;}function proxyFunctions(trackedEntity, options) {if(typeof trackedEntity==='function') return;Object.getOwnPropertyNames(trackedEntity).forEach((name)=> {if(typeof trackedEntity[name]==='function') { trackedEntity[name]=newProxy(trackedEntity[name], { apply: trackFunctionCall(options),});}});}
可以看到,假如我們希望監測對象的屬性,我們創建了一個帶有 get 和 set 攔截器的被監測對象。下面是 set 攔截器的實現:
function trackPropertySet(options={}) {returnfunctionset(target, prop, value, receiver) {const{ trackCaller, trackCount, stdout, filter }=options;const error=trackCaller && newError();const caller=getCaller(error);const contextName=target.constructor.name==='Object'? '': `${target.constructor.name}.`;const name=`${contextName}${prop}`;const hashKey=`set_${name}`;if(trackCount) {if(!callerMap[hashKey]) { callerMap[hashKey]=1;} else{ callerMap[hashKey]++;}} let output=`${name} is being set`;if(trackCaller) { output +=` by ${caller.name}`;}if(trackCount) { output +=` for the ${callerMap[hashKey]} time`;} let canReport=true;if(filter) { canReport=filter({ type: 'get', prop, name, caller, count: callerMap[hashKey], value,});}if(canReport) {if(stdout) { stdout(output);} else{ console.log(output);}}returnReflect.set(target, prop, value, receiver);};}
更有趣的是 trackClass 函數(至少對我來說是這樣):
function trackClass(cls, options={}) { cls.prototype=trackObject(cls.prototype, options); cls.prototype.constructor=cls; returnnewProxy(cls, { construct(target, args) {const obj=new target(...args);returnnewProxy(obj, {get: trackPropertyGet(options),set: trackPropertySet(options),});}, apply: trackFunctionCall(options),});}
在這個案例中,因為我們希望攔截這個類上不屬于原型上的屬性,所以我們給這個類的原型創建了個代理,并且創建了個構造函數攔截器。
別忘了,即使你在原型上定義了一個屬性,但如果你再給這個對象賦值一個同名屬性,JavaScript 將會創建一個這個屬性的本地副本,所以賦值的改動并不會改變這個類其他實例的行為。這就是為何只對原型做代理并不能滿足要求的原因。
程內容
1.1、函數的概念
函數是由事件驅動的或者當它被調用時執行的可重復使用的代碼塊。
函數過程中的這些語句用于完成某些有意義的工作——通常是處理文本,控制輸入或計算數值。
通過在程序代碼中引入函數名稱和所需的參數,可在該程序中執行(或稱調用)該函數。
1.2、函數詳解
函數是由關鍵字 function、函數名、一組參數,以及置于括號中的待執行代碼所組成的。
當調用該函數時,會創建一個臨時空間(閉包),并且執行函數內的代碼。
可以在某事件發生時直接調用函數(比如當用戶點擊按鈕時),并且可由 JavaScript 在任何位置進行調用。
調用帶參數的函數
在調用函數時,您可以向其傳遞值,這些值被稱為參數。
這些參數可以在函數中使用。
您可以發送任意多的參數,由逗號 (,) 分隔。
當您聲明函數時,請把參數作為變量來聲明。
變量和參數必須以一致的順序出現。第一個變量就是第一個被傳遞的參數的給定的值,以此類推。
帶有返回值的函數
有時,我們會希望函數將值返回調用它的地方。
通過使用 return 語句就可以實現。
在使用 return 語句時,函數會停止執行,并返回指定的值。
整個 JavaScript 并不會停止執行,僅僅是函數。JavaScript 將繼續執行代碼,從調用函數的地方。
js函數調用的參數就是 this 和 arguments 。arguments是參數組,他并不是一個真實的數組,但是可以使用.length方法獲得長度。
函數有四種調用模式
一、函數調用形式
所謂函數形式就是一般聲明函數后直接調用
二、方法調用模式
同樣的是函數,將其賦值給一個對象的成員以后就不一樣了. 將函數賦值給對象的成員后,那么這個就不在稱為函數,而應該叫做方法.
三、構造器調用模式
同樣是函數,在單純的函數模式下,this 表示 window;在對象方法模式下,this 指 的是當前對象. 除了這兩種情況,JavaScript 中函數還可以是構造器. 將函數作為構造器來使用的語法就是在函數調用前面加上一個 new 關鍵字. (關于this關鍵字下面即將講到)
四、apply調用模式
除了上述三種調用模式以外,函數作為對象還有 apply 方法與 call 方法可以使用, apply 模式既可以像函數一樣使用,也可以像方法一樣使用,是一個靈活的使用方法。
JavaScript 對大小寫敏感。關鍵詞 function 必須是小寫的,并且必須以與函數名稱相同的大小寫來調用函數。
傳值和傳引用(傳地址)
1.3、閉包的概念
閉包就是能夠讀取其他函數內部變量的函數。
在Javascript語言中,只有函數內部的子函數才能讀取局部變量,因此可以把閉包簡單理解成"定義在一個函數內部的函數"。
在本質上,閉包就是將函數內部和函數外部連接起來的一座橋梁。
1.4、基于函數執行的內存解析(閉包使用)
由于閉包會使得函數中的變量都被保存在內存中,內存消耗很大,所以不能濫用閉包,否則會造成網頁的性能問題,在IE中可能導致內存泄露。解決方法是,在退出函數之前,將不使用的局部變量全部刪除。
閉包會在父函數外部,改變父函數內部變量的值。所以,如果你把父函數當作對象(object)使用,把閉包當作它的公用方法(Public Method),把內部變量當作它的私有屬性(private value),這時一定要小心,不要隨便改變父函數內部變量的值。
閉包是通過函數實現的一塊動態的、臨時的內存空間,在函數調用時動態產生,調用完即銷毀,在這個過程中里面定義的變量和函數都是臨時的(局部的)
1.5、對象的概念
萬事萬物皆對象,每一個對象可以有自己的屬性、方法、標識,這意味著每一個對象都可以擁有自己的內部數據(這些數據通過對象的屬性來表示)和行為(行為通過對象的方法來體現),內存中的每一個對象都可以通過一個標識(這個標識指的就是對象在內存中的地址)唯一的和其他對象區分開來.
1.6、對象所在內存區域
對象所在的內存區域就是堆內存
1.7、javascript中對象的語法
創建 JavaScript 對象
通過 JavaScript,您能夠定義并創建自己的對象。
創建新對象有兩種不同的方法:
1.定義并創建對象的實例
2.使用函數來定義對象,然后創建新的對象實例
javascript操作對象的特色寫法
var obj=new Object(); 以及 var obj={}; 這兩種創建對象的方式結果是一樣的
而通過后者var obj={name:”aaa”,say:function(){}};可以實現對對象屬性和功能的初始化定義
訪問對象的屬性
屬性是與對象相關的值。
訪問對象屬性的語法是:
objectName.propertyName
訪問對象的方法
方法是能夠在對象上執行的動作。
您可以通過以下語法來調用方法:
objectName.methodName()
1.8、封裝的概念
生活中的封裝是指:
將某些東西包裝在一起,然后以新的完整形式呈現出來;
程序中的封裝是指:
隱藏對象的屬性和實現細節,僅對外提供公共的訪問方式
封裝其實就是有選擇性地公開或隱藏某些信息,它解決了數據的安全性問題,便于維護和擴展。
在javascript中通過構造器進行封裝的實現
1.9、Function構造器函數和new關鍵字
JavaScript中function定義函數有兩種作用:
1. 作為普通的函數聲明使用。
之前我們所有針對函數的講解都是這種使用方式
2. 作為類構造器函數聲明使用。
構造器函數結合new關鍵字來調用,返回對象的內存地址,之后可以通過操作這個對象(內存地址)的屬性和方法來實現業務邏輯
1.10、this關鍵字
this代表的是當前內存空間的引用。
通過this顯示的去使用當前對象的成員(屬性、方法)
1.11、prototype原型
Prototype原型模式是一種創建型設計模式,Prototype模式允許一個對象再創建另外一個可定制的對象,根本無需知道任何如何創建的細節
工作原理是:通過將一個原型對象傳給那個要發動創建的對象,這個要發動創建的對象通過請求原型對象拷貝它們自己來實施創建。
1.12、for循環迭代對象屬性
1.14、typeof關鍵字的使用
JS中的變量是松散類型(即弱類型)的,可以用來保存任何類型的數據。
typeof 可以用來檢測給定變量的數據類型。
1.15、instanceof關鍵字的使用
instanceof關鍵字用于判斷一個引用類型變量所指向的對象是否是一個類(或接口、抽象類、父類)的實例。
1.16、普通函數和構造器函數的區別
共同點:
只要是函數,都需要被調用,只要被調用都會創建閉包。
異同點:
普通函數調用直接在函數名字后面加上一對括號;構造器函數使用new關鍵字來調用。
普通函數調用關注的是執行函數的過程和返回結果;構造器函數調用關注的返回的內存空間(閉包)。.
1.17、作用域
作用域是指對某一變量和方法具有訪問權限的代碼空間
在Javascript中, 作用域是在函數中維護的。
1.18、繼承的概念
繼承是指一個對象直接使用另一對象的屬性和方法。
繼承可以使得子類具有父類的各種屬性和方法,而不需要再次編寫相同的代碼。
在令子類繼承父類的同時,可以重新定義某些屬性,并重寫某些方法,即覆蓋父類的原有屬性和方法,使其獲得與父類不同的功能。
喜歡請關注!點贊 評論!
今日微語:
坐在秋天的曠野里,把一地凄清,守候成滿山淡黃的野菊。生命若歌,起伏跌宕,聲起聲落,我們每個人都是歌者;浮華塵世,生命如茶,或濃或淡,或苦或甜,需要我們用心去品嘗。記住該記住的,忘記該忘記的,改變能改變的,接受不能接受的,也許,我們無法把握未來,但我們起碼可以左右現在,不是嗎?喜歡與你默默對視,喜歡在心的曠野里,與你纏綿相依。時光荏苒,無關風月,只是,愿意在你的注視下,輕執墨痕,為你寫一段文字。一縷清風,一朵小花,一個微笑,一句輕聲的問候,就夠了。愛,無需刻意的裝飾。
今年國慶假期終于可以憋在家里了不用出門了,不用出去看后腦了,真的是一種享受。這么好的光陰怎么浪費,睡覺、吃飯、打豆豆這怎么可能(耍多了也煩),完全不符合我們程序員的作風,趕緊起來把文章寫完。
這篇文章比較基礎,在國慶期間的業余時間寫的,這幾天又完善了下,力求把更多的前端所涉及到的關于文件上傳的各種場景和應用都涵蓋了,若有疏漏和問題還請留言斧正和補充。
以下是本文所涉及到的知識點,break or continue ?
原理很簡單,就是根據 http 協議的規范和定義,完成請求消息體的封裝和消息體的解析,然后將二進制內容保存到文件。
我們都知道如果要上傳一個文件,需要把 form 標簽的enctype設置為multipart/form-data,同時method必須為post方法。
那么multipart/form-data表示什么呢?
multipart互聯網上的混合資源,就是資源由多種元素組成,form-data表示可以使用HTML Forms 和 POST 方法上傳文件,具體的定義可以參考RFC 7578。
multipart/form-data 結構
看下 http 請求的消息體
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryDCntfiXcSkPhS4PN 表示本次請求要上傳文件,其中boundary表示分隔符,如果要上傳多個表單項,就要使用boundary分割,每個表單項由———XXX開始,以———XXX結尾。
每一個表單項又由Content-Type和Content-Disposition組成。
Content-Disposition: form-data 為固定值,表示一個表單元素,name 表示表單元素的 名稱,回車換行后面就是name的值,如果是上傳文件就是文件的二進制內容。
Content-Type:表示當前的內容的 MIME 類型,是圖片還是文本還是二進制數據。
解析
客戶端發送請求到服務器后,服務器會收到請求的消息體,然后對消息體進行解析,解析出哪是普通表單哪些是附件。
可能大家馬上能想到通過正則或者字符串處理分割出內容,不過這樣是行不通的,二進制buffer轉化為string,對字符串進行截取后,其索引和字符串是不一致的,所以結果就不會正確,除非上傳的就是字符串。
不過一般情況下不需要自行解析,目前已經有很成熟的三方庫可以使用。
至于如何解析,這個也會占用很大篇幅,后面的文章在詳細說。
使用 form 表單上傳文件
在 ie時代,如果實現一個無刷新的文件上傳那可是費老勁了,大部分都是用 iframe 來實現局部刷新或者使用 flash 插件來搞定,在那個時代 ie 就是最好用的瀏覽器(別無選擇)。
DEMO
這種方式上傳文件,不需要 js ,而且沒有兼容問題,所有瀏覽器都支持,就是體驗很差,導致頁面刷新,頁面其他數據丟失。
HTML
<form method="post" action="http://localhost:8100" enctype="multipart/form-data">
選擇文件:
<input type="file" name="f1"/> input 必須設置 name 屬性,否則數據無法發送<br/>
<br/>
標題:<input type="text" name="title"/><br/><br/><br/>
<button type="submit" id="btn-0">上 傳</button>
</form>
復制代碼
服務端文件的保存基于現有的庫koa-body結合 koa2實現服務端文件的保存和數據的返回。
在項目開發中,文件上傳本身和業務無關,代碼基本上都可通用。
在這里我們使用koa-body庫來實現解析和文件的保存。
koa-body 會自動保存文件到系統臨時目錄下,也可以指定保存的文件路徑。
然后在后續中間件內得到已保存的文件的信息,再做二次處理。
NODE
/**
* 服務入口
*/
var http=require('http');
var koaStatic=require('koa-static');
var path=require('path');
var koaBody=require('koa-body');//文件保存庫
var fs=require('fs');
var Koa=require('koa2');
var app=new Koa();
var port=process.env.PORT || '8100';
var uploadHost=`http://localhost:${port}/uploads/`;
app.use(koaBody({
formidable: {
//設置文件的默認保存目錄,不設置則保存在系統臨時目錄下 os
uploadDir: path.resolve(__dirname, '../static/uploads')
},
multipart: true // 開啟文件上傳,默認是關閉
}));
//開啟靜態文件訪問
app.use(koaStatic(
path.resolve(__dirname, '../static')
));
//文件二次處理,修改名稱
app.use((ctx)=> {
var file=ctx.request.files.f1;//得道文件對象
var path=file.path;
var fname=file.name;//原文件名稱
var nextPath=path+fname;
if(file.size>0 && path){
//得到擴展名
var extArr=fname.split('.');
var ext=extArr[extArr.length-1];
var nextPath=path+'.'+ext;
//重命名文件
fs.renameSync(path, nextPath);
}
//以 json 形式輸出上傳文件地址
ctx.body=`{
"fileUrl":"${uploadHost}${nextPath.slice(nextPath.lastIndexOf('/')+1)}"
}`;
});
/**
* http server
*/
var server=http.createServer(app.callback());
server.listen(port);
console.log('demo1 server start ...... ');
復制代碼
CODE
https://github.com/Bigerfe/fe-learn-code/
*請認真填寫需求信息,我們會在24小時內與您取得聯系。