Part1 手寫代碼
現場手寫代碼是現在面試中很常見的一類面試題,考察基礎的數據結構與算法能力。
1 數組去重的實現
基本數組去重
Array.prototype.unique = function(){ var result = []; this.forEach(function(v){ if(result.indexOf(v) < 0){ result.push(v); } }); return result; }
•利用hash表去重,這是一種空間換時間的方法
Array.prototype.unique = function(){ var result = [],hash = {}; this.forEach(function(v){ if(!hash[v]){ hash[v] = true; result.push(v); } }); return result; }
上面的方法存在一個bug,對於數組[1,2,'1','2',3],去重結果為[1,2,3],原因在於對象對屬性索引時會進行強制類型轉換,arr[‘1']和arr[1]得到的都是arr[1]的值,因此需做一些改變:
Array.prototype.unique = function(){ var result = [],hash = {}; this.forEach(function(v){ var type = typeof(v); //獲取元素類型 hash[v] || (hash[v] = new Array()); if(hash[v].indexOf(type) < 0){ hash[v].push(type); //存儲類型 result.push(v); } }); return result; }
•先排序後去重
Array.prototype.unique = function(){ var result = [this[0]]; this.sort(); this.forEach(function(v){ v != result[result.length - 1] && result.push(v); //僅與result最後一個元素比較 }); }
2 快速排序的實現
方法一(盡可能不用js數組方法):
function quickSort(arr){ qSort(arr,0,arr.length - 1); } function qSort(arr,low,high){ if(low < high){ var partKey = partition(arr,low,high); qSort(arr,low, partKey - 1); qSort(arr,partKey + 1,high); } } function partition(arr,low,high){ var key = arr[low]; //使用第一個元素作為分類依據 while(low < high){ while(low < high && arr[high] >= arr[key]) high--; arr[low] = arr[high]; while(low < high && arr[low] <= arr[key]) low++; arr[high] = arr[low]; } arr[low] = key; return low; }
方法二(使用js數組方法):
function quickSort(arr){ if(arr.length <= 1) return arr; var index = Math.floor(arr.length/2); var key = arr.splice(index,1)[0]; var left = [],right = []; arr.forEach(function(v){ v <= key ? left.push(v) : right.push(v); }); return quickSort(left).concat([key],quickSort(right)); }
另外要知道,快速排序的平均時間復雜度O(nlogn),最壞情況是有序的情況,時間復雜度為n的平方,另外快速排序是不穩定的。
Part2 JavaScript相關
1 JavaScript基礎數據類型
JavaScript數據類型包括原始類型和引用類型,原始類型有五個:
Number(數值) String(字符串) Boolean(布爾) Null(空) Undefined(未定義)
引用類型有一個:
Object(對象)
通過typeof(x)可以返回一個變量x的數據類型“number”、“string”、“boolean”、“undefined”、"object",這裡要注意一點:typeof運算符對於null類型返回的是object。
《JavaScript高級程序設計》:
這實際上是JavaScript最初實現中的一個錯誤,後來被ECMAScript沿用了。現在null被認為是對象的占位符,從而解釋了這一矛盾。但是從技術上來說,它仍然是原始值。
2 談一談JavaScript作用域鏈
當執行一段JavaScript代碼(全局代碼或函數)時,JavaScript引擎會創建為其創建一個作用域又稱為執行上下文(Execution Context),在頁面加載後會首先創建一個全局的作用域,然後每執行一個函數,會建立一個對應的作用域,從而形成了一條作用域鏈。每個作用域都有一條對應的作用域鏈,鏈頭是全局作用域,鏈尾是當前函數作用域。
作用域鏈的作用是用於解析標識符,當函數被創建時(不是執行),會將this、arguments、命名參數和該函數中的所有局部變量添加到該當前作用域中,當JavaScript需要查找變量X的時候(這個過程稱為變量解析),它首先會從作用域鏈中的鏈尾也就是當前作用域進行查找是否有X屬性,如果沒有找到就順著作用域鏈繼續查找,直到查找到鏈頭,也就是全局作用域鏈,仍未找到該變量的話,就認為這段代碼的作用域鏈上不存在x變量,並拋出一個引用錯誤(ReferenceError)的異常。
3 如何理解JavaScript原型鏈
JavaScript中的每個對象都有一個prototype屬性,我們稱之為原型,而原型的值也是一個對象,因此它也有自己的原型,這樣就串聯起來了一條原型鏈,原型鏈的鏈頭是object,它的prototype比較特殊,值為null。
原型鏈的作用是用於對象繼承,函數A的原型屬性(prototype property)是一個對象,當這個函數被用作構造函數來創建實例時,該函數的原型屬性將被作為原型賦值給所有對象實例,比如我們新建一個數組,數組的方法便從數組的原型上繼承而來。
當訪問對象的一個屬性時, 首先查找對象本身, 找到則返回; 若未找到, 則繼續查找其原型對象的屬性(如果還找不到實際上還會沿著原型鏈向上查找, 直至到根). 只要沒有被覆蓋的話, 對象原型的屬性就能在所有的實例中找到,若整個原型鏈未找到則返回undefined;
4 JavaScript變量聲明提前
《JavaScript權威指南》中是這樣解釋的:JavaScript變量在聲明之前已經可用,JavaScript的這個特性被非正式的稱為聲明提前(hoisting),即JavaScript函數中聲明的所有變量(但不涉及賦值)都被“提前”至函數的頂部。
從一個例子來看:
var scope = "global"; function myFunc(){ console.log(scope); var scope = "local"; }
控制台打印出來的不是“global”而是“undefined”,這是因為在myFunc這個函數的作用域中,局部變量scope聲明被提前至函數頂部,而此時,scope僅聲明,未賦值,因此輸出undefined。實際上,上面的代碼和下面的效果是一樣的:
var scope = "global"; function myFunc(){ var scope; console.log(scope); scope = "local"; }
5 如何理解和應用JavaScript閉包
關於閉包具體的定義文獻中給的概念很抽象,我認為閉包是一種使函數能夠都去其它函數的局部變量的語法機制。
舉個例子:
function outFunc(){ var name = "Vicfeel"; function inFunc(){ console.log(name); } return inFunc; } inFunc(); //控制台顯示"Vicfeel"
這這個例子我們可以看出,在函數inFunc中依然可以訪問outFunc的局部變量name。
閉包應用舉例,模擬類的私有屬性,利用閉包的性質,局部變量只有在sayAge方法中才可以訪問,而name在外部也訪問,從而實現了類的私有屬性。
function User(){ this.name = "Vicfeel"; //共有屬性 var age = 23; //私有屬性 this.sayAge:function(){ console.log("my age is " + age); } } var user = new User(); console.log(user.name); //"Vicfeel" console.log(user.age); //"undefined" user.sayAge(); //"my age is 23"
要了解詳細的閉包,推薦一下 阮一峰的網絡日志-學習Javascript閉包(Closure)。
6 new構建對象的本質
function User(){ this.name = "Vicfeel"; this.age = 23; } var user = new User();
通過new操作符,實際上在構造函數User中完成了如下操作:
•創建一個新的對象,這個對象的類型是object;
•設置這個新的對象的內部、可訪問性和prototype屬性為構造函數(指prototype.construtor所指向的構造函數)中設置的;
•執行構造函數;
•返回新創建的對象。
function User(){ //this = {}; //this.constructor = User; this.name = "Vicfeel"; this.age = 23; //return this; } var user = new User();
如果構造函數默認返回的新創建的this對象,如果手動return 一個變量的話,如果該變量是原始類型則無效,如果是對象,則返回該對象。
7 JavaScript代理
當我們需要對很多元素添加事件的時候,可以通過將事件添加到它們的父節點而將事件委托給父節點來觸發處理函數。
比如我們需要向一個ul中動態添加很多個li,需要遍歷li逐個添加點擊事件
<ul id='list'></ul> var count = 100; var ulList = document.getElementById("list"); //動態構建節點 for(var i = count;i--;){ var liDom = document.createElement('li'); ulList.appendChild(liDom); } //綁定點擊事件 var liNode = ulList.getElementByTagName("li"); for(var i=0, l = liNodes.length; i < l; i++){ liNode[i].onClick = function(){ //li點擊事件 } }
眾所周知,DOM操作是十分消耗性能的。所以重復的事件綁定簡直是性能殺手。而事件代理的核心思想,就是通過盡量少的綁定,去監聽盡量多的事件。如何做呢?答案是利用事件冒泡機制,對其父節點ul進行事件綁定(Event Bubble),然後通過event.target來判斷是哪個節點觸發的事件,從而減少很多EventHandler的綁定。
var count = 100; var ulList = document.getElementById("list"); //動態構建節點 for(var i = count;i--;){ var liDom = document.createElement('li'); ulList.appendChild(liDom); } //綁定點擊事件 var liNode = ulList.getElementByTagName("li"); liNode.onClick = function(e){ if(e.target && e.target.nodeName.toUpperCase == "LI") { // li點擊事件 } }
發現新內容會持續更新...