一、前言
昨天被朋友問道了一個關於js的題目,據他說是網上的一道面試題,我看了一下。忽然想起了以前自己參加面試時候的一些場景:
某一天收到了一個野雞公司的面試通知,可還沒有工作的我依然心花怒放,遂梳妝打扮,沐浴焚香,經過幾個小時的精心准備,懷揣着一顆赤誠的心,提前兩個小時來到面試地點。面試地點是一段繁華地段的CBD,走出電梯,高大上的裝修瞬間讓我有了種劉姥姥進大觀園的感覺,公司氣氛異常熱鬧,每個人都緊張的忙碌着,不時的嘴里喊道‘Tom啊,我的idea你think一下啊’。然后一個西裝筆挺的小哥把我帶到一個嚴肅的會議室,扔給我幾張紙並用鼻孔看着我說道:‘這是筆試題,做吧’,然后頭也不回的走掉。沒見過世面的我緊張極了,觸摸着那幾張紙久久不敢打開。心中無限幻想:這么牛逼的公司,這些題不會我一道都做不上吧?不會都是英文寫的吧?考的都是高大上的內容吧,我能看懂嗎?掙扎中,我緩緩打開,面試題映入眼簾:
1、屬性float的作用是?
2、css3新增了那些屬性?
3、js中call函數有什么功能?
4、...
情景是我扯的,但是這樣的事情我相信很多人都遇到過。只是想說明一些問題,有些公司似乎在提問題上面並不願意下很多功夫,除去一些其他的原因以外,那我只能說誠意不夠了,既是只是要招聘一個很初級的程序員。當然也並不是說這些題目本身有問題,深挖的話都會牽扯到許多知識。而是這些公司的做法,隨便到網上搜幾道題,攢到一起,就湊成了一套面試題,沒有明確的考察方向,隨便考一下了事,我作為應聘者對於這樣的公司就一個想法:不走心啊!
畢竟技術文章,過多內容不說,單說下面這道題目,難度不大,但綜合考察了很多基礎的內容,個人認為不錯。這里就稍作分析,過程中有什么問題,歡迎斧正。
// 請實現下面的自定義事件 Event 對象的接口,功能見注釋(測試1) // 該 Event 對象的接口需要能被其他對象拓展復用(測試2) // 測試1 Event.on('test', function (result) { console.log(result); }); Event.on('test', function () { console.log('test'); }); Event.emit('test', 'hello world'); // 輸出 'hello world' 和 'test' // 測試2 var person1 = {}; var person2 = {}; Object.assign(person1, Event); Object.assign(person2, Event); person1.on('call1', function () { console.log('person1'); }); person2.on('call2', function () { console.log('person2'); }); person1.emit('call1'); // 輸出 'person1' person1.emit('call2'); // 沒有輸出 person2.emit('call1'); // 沒有輸出 person2.emit('call2'); // 輸出 'person2' var Event = { // 通過on接口監聽事件eventName // 如果事件eventName被觸發,則執行callback回調函數 on: function (eventName, callback) { //你的代碼 }, // 觸發事件 eventName emit: function (eventName) { //你的代碼 } };
二、題目分析
這道題,分為兩個部分,測試1考察的主要內容是自定義事件中的綁定與觸發,測試2主要考察內容為ES6規范中Object.assign方法和對象屬性方法中Object.defineProperty的應用,以及普通對象和引用對象的區別。整體發布-訂閱模式的一種實現,也就是設計模式中的觀察者模式。(設計模式不做過多展開,有興趣自行搜索),讓我們先來分析一下自定義事件。
2.1 自定義事件
js中的自定義事件與js事件的關系可以說是雷鋒和雷峰塔的關系,不過是自定義事件擁有了一些類似事件的特性,所以,類比於js事件而成為自定義事件。自定義事件在組件開發中是一種比較常見的方式,可以有效的解決沖突與覆蓋問題。
自定義事件的實質是函數。為事件對象添加對應的事件屬性,屬性下面對應着該屬性的方法,要觸發此事件時,依次調用該事件下方法即可。
自定事件的綁定是要構造一個形如下面代碼的結構
Event = { // 事件名稱 event1 : [fn1, fn2, ...], event2 : [fn3, fn4, ...], ... }
如果要觸發某個自定義事件,實質也就是調用相應的函數即可,如調用Event對象下面的event1事件所綁定的所有方法
for( var i = 0; i < Event.event1.length; i++ ){ Event.event1[i](); }
來看一下js原生事件的綁定
// 事件綁定 htmlElem.addEventListener('click', function (){ // do something }, false);
這段程序為DOM元素htmlElem綁定了一個click事件,觸發的方式為當鼠標點擊這個元素的時候即可觸發。然而,自定義事件既然名為自定義,就說明是我們附加的行為,需要自己想辦法來觸發,點擊顯然不行,砸了顯示器也不會觸發。原生的js事件多是綁定在DOM元素,或者XMLHttpRequest等對上,屬於瀏覽器行為事件。自定義事件的本質是函數,所以綁定並沒有這樣的限制。根據以上原理可編寫簡易的自定義事件綁定與觸發方法。
// 自定義事件的綁定 function bindEvent ( obj, eventName, fn ){ obj.listeners = obj.listeners || {}; obj.listeners[eventName] = obj.listeners[eventName] || []; obj.listeners[eventName].push( fn ); } // 自定義事件觸發 function fireEvent ( obj, eventName ){ if( !(obj.listeners && obj.listeners[eventName]) ) return; for( var i = 0; i < obj.listeners[eventName].length; i++ ){ obj.listeners[eventName][i](); } }
自定義事件綁定的方法構建了事件對象、事件名稱和事件函數的一個映射關系。而事件的觸發是通過調用事件對象對應事件名稱屬性上的方法來實現。這里只是簡單說明了一下自定義事件綁定的基本原理,更多細節如事件解綁、原生事件綁定等等與此題聯系不大,不做展開。
2.2 Object.assign方法
這個方法是ES6規范中新增加的一個API,作用是將所有可枚舉的屬性的值從一個或多個源對象復制到目標對象。這是官方文檔上的說法,用人話來說就是:合並對象。Object.assign MDN文檔
用法就是:Object.assign(target, obj1, obj2, ...)
實際的效果就是依次的將obj1 obj2中的屬性合並到target中,如果鍵相同,后面的會覆蓋掉前面的。但是屬性必須是自身的(不能是繼承的屬性)或者可枚舉的(訪問器屬性中enumerable屬性值為true),返回目標對象,也就是合並后的結果。舉個栗子:
2.2.1 Object.assign的合並與覆蓋的特性
var a = {}; var b = { name : 'zhangsan' } var c = Object.assign(a, b); console.log(c); // c : { name : 'zhangsan' } var d = { name : 'lisi' } var e = Object.assign(c, d); console.log(e); // e : { name : 'lisi' } var f = { age : 10000 } var g = Object.assign(e, f); console.log(g); // g : { name: "lisi", age: 10000 }
2.2.2 Object.assign 可合並自身可枚舉屬性
可合並的屬性有兩個條件:自身(非繼承),可枚舉(enumerable值為true)
// 利用Object.create方法創建obj對象,obj包含三個屬性,不可枚舉的value2,可枚舉的value3和可通過原型鏈找到的繼承屬性value1 var obj = Object.create({ value1 : 1 }, { value2 : { value : 2 }, value3 : { value : 3, enumerable : true } }); console.log(obj); // { value2 : 2, value3 : 3 } var copy = Object.assign({}, obj); console.log(copy); // { value3 : 3 }
兩次打印效果如圖:(btw:就在我剛剛截圖的時候才注意到,不可枚舉的屬性在chrome調試工具中顯示的顏色同可枚舉屬性是有差別的,以前從沒注意過。)
2.2.3 深度克隆
這里算得上這道問題的一個小坑吧。Object.assign並不能實現深度克隆,也就是說源對象如果是一個引用對象,那么它拷貝的僅僅是應用而並不是引用對象本身。
var arr = [1, 2, 3]; var a = { b : arr } console.log(a); // a : { b : [1, 2, 3] } var c = Object.assign({}, a); console.log(c); // c : { b : [1, 2, 3] } // 向a對象中的b添加一個值 a.b.push(4); console.log(c); // c : { b : [1, 2, 3, 4] }
三、解決方法
基於以上的基礎知識與分析,解答此題
var Event = { on : function ( eventName, fn ){ var _this = this; !this.listeners && (function (){ Object.defineProperty(_this, 'listeners', { value: {}, enumerable: false }); })(); this.listeners[eventName] = this.listeners[eventName] || []; this.listeners[eventName].push( fn ); }, emit : function ( eventName ){ if( !(this.listeners && this.listeners[eventName]) ) return; for( var i = 0; i < this.listeners[eventName].length; i++ ){ this.listeners[eventName][i]( arguments[1] ); } } }
經過測試,可以完成題目所需要求。
四、總結
此題主要涉及的技術,自定義事件、Object.assign的用法、對象數據屬性設置、引用對象復制問題、arguments問題等。
總體來看是一道精心准備的題目,走心了!。其中有很多地方也沒有展開來說,這里也是做一個簡單的原理解釋,其他內容放在其他篇幅里解釋吧。
如果覺得這篇文章對你還有那么點點點點作用,點個推薦吧。