摘要:雖然構造函數或者對象字面量的方法都可以用來創建對象,但是這些方法使用同一個接口創建很多對象,會產生大量的重復代碼。參考資料冴羽的專題系列中高級前端面試手寫代碼無敵秘籍前端筆試之手寫代碼一
本系列會從面試的角度出發圍繞JavaScript,Node.js(npm包)以及框架三個方面來對常見的模擬實現進行總結,具體源代碼放在github項目上,長期更新和維護
數組去重
(一維)數組去重最原始的方法就是使用雙層循環,分別循環原始數組和新建數組;或者我們可以使用indexOf來簡化內層的循環;或者可以將原始數組排序完再來去重,這樣會減少一個循環,只需要比較前后兩個數即可;當然我們可以使用ES5,ES6的方法來簡化去重的寫法,比如我們可以使用filter來簡化內層循環,或者使用Set、Map、擴展運算符這些用起來更簡單的方法,但是效率上應該不會比原始方法好。二維數組的去重可以在上面方法的基礎上再判斷元素是不是數組,如果是的話,就進行遞歸處理。
雙層循環
var array = [1, 1, "1", "1"]; function unique(array) { var res = []; for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { for (var j = 0, resLen = res.length; j < resLen; j++ ) { if (array[i] === res[j]) { break; } } if (j === resLen) { res.push(array[i]) } } return res; } console.log(unique(array)); // [1, "1"]
利用indexOf
var array = [1, 1, "1"]; function unique(array) { var res = []; for (var i = 0, len = array.length; i < len; i++) { var current = array[i]; if (res.indexOf(current) === -1) { res.push(current) } } return res; } console.log(unique(array));
排序后去重
var array = [1, 1, "1"]; function unique(array) { var res = []; var sortedArray = array.concat().sort(); var seen; for (var i = 0, len = sortedArray.length; i < len; i++) { // 如果是第一個元素或者相鄰的元素不相同 if (!i || seen !== sortedArray[i]) { res.push(sortedArray[i]) } seen = sortedArray[i]; } return res; } console.log(unique(array));
filter
filter可以用來簡化外層循環
使用indexOf:
var array = [1, 2, 1, 1, "1"]; function unique(array) { var res = array.filter(function(item, index, array){ return array.indexOf(item) === index; }) return res; } console.log(unique(array));
排序去重:
var array = [1, 2, 1, 1, "1"]; function unique(array) { return array.concat().sort().filter(function(item, index, array){ return !index || item !== array[index - 1] }) } console.log(unique(array));
ES6方法
Set:
var array = [1, 2, 1, 1, "1"]; function unique(array) { return Array.from(new Set(array)); } console.log(unique(array)); // [1, 2, "1"]
再簡化下
function unique(array) { return [...new Set(array)]; } //或者 var unique = (a) => [...new Set(a)]
Map:
function unique (arr) { const seen = new Map() return arr.filter((a) => !seen.has(a) && seen.set(a, 1)) }
類型判斷
類型判斷需要注意以下幾點
typeof對六個基本數據類型Undefined、Null、Boolean、Number、String、Object(大寫)返回的結果是
undefined、object、boolean、number、string、object(小寫),可以看到Null和Object 類型都返回了 object 字符串;typeof卻能檢測出函數類型;綜上,typeof能檢測出六種類型,但是不能檢測出null類型和Object下細分的類型,如Array,Function,Date,RegExp,Error等。
Object.prototype.toString的作用非常強大,它能檢測出基本數據類型以及Object下的細分類型,甚至像 Math,JSON,arguments它都能檢測出它們的具體類型,它返回結果形式例如[object Number](注意最后的數據類型是大寫).所以,Object.prototype.toString基本上能檢測出所有的類型了,只不過有時需要考慮到兼容性低版本瀏覽器的問題。
通用API
// 該類型判斷函數可以判斷六種基本數據類型以及Boolean Number String Function Array Date RegExp Object Error, // 其他類型因為遇到類型判斷的情況較少所以都會返回object,不在進行詳細的判斷 // 比如ES6新增的Symbol,Map,Set等類型 var classtype = {}; "Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item) { classtype["[object " + item + "]"] = item.toLowerCase(); }) function type(obj) { // 解決IE6中null和undefined會被Object.prototype.toString識別成[object Object] if (obj == null) { return obj + ""; } //如果是typeof后類型為object下的細分類型(Array,Function,Date,RegExp,Error)或者是Object類型,則要利用Object.prototype.toString //由于ES6新增的Symbol,Map,Set等類型不在classtype列表中,所以使用type函數,返回的結果會是object return typeof obj === "object" || typeof obj === "function" ? classtype[Object.prototype.toString.call(obj)] || "object" : typeof obj; }
判斷空對象
判斷是否有屬性,for循環一旦執行,就說明有屬性,此時返回false
function isEmptyObject( obj ) { var name; for ( name in obj ) { return false; } return true; } console.log(isEmptyObject({})); // true console.log(isEmptyObject([])); // true console.log(isEmptyObject(null)); // true console.log(isEmptyObject(undefined)); // true console.log(isEmptyObject(1)); // true console.log(isEmptyObject("")); // true console.log(isEmptyObject(true)); // true
我們可以看出isEmptyObject實際上判斷的并不僅僅是空對象。但是既然jQuery是這樣寫,可能是因為考慮到實際開發中 isEmptyObject用來判斷 {} 和 {a: 1} 是足夠的吧。如果真的是只判斷 {},完全可以結合上篇寫的 type函數篩選掉不適合的情況。
判斷Window對象
Window對象有一個window屬性指向自身,可以利用這個特性來判斷是否是Window對象
function isWindow( obj ) { return obj != null && obj === obj.window; }
判斷數組
isArray是數組類型內置的數據類型判斷函數,但是會有兼容性問題,一個polyfill如下
isArray = Array.isArray || function(array){ return Object.prototype.toString.call(array) === "[object Array]"; }
判斷類數組
jquery實現的isArrayLike,數組和類數組都會返回true。所如果isArrayLike返回true,至少要滿足三個條件之一:
是數組
長度為 0 比如下面情況,如果我們去掉length === 0 這個判斷,就會打印 false,然而我們都知道 arguments 是一個類數組對象,這里是應該返回 true 的
function a(){ console.log(isArrayLike(arguments)) } a();
lengths 屬性是大于 0 的數字類型,并且obj[length - 1]必須存在(考慮到arr = [,,3]的情況)
function isArrayLike(obj) { // obj 必須有 length屬性 var length = !!obj && "length" in obj && obj.length; var typeRes = type(obj); // 排除掉函數和 Window 對象 if (typeRes === "function" || isWindow(obj)) { return false; } return typeRes === "array" || length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj; }
判斷NaN
判斷一個數是不是NaN不能單純地使用 === 這樣來判斷, 因為NaN不與任何數相等, 包括自身,注意在ES6的isNaN中只有值為數字類型使用NaN才會返回true
isNaN: function(value){ return isNumber(value) && isNaN(value); }
判斷DOM元素
利用DOM對象特有的nodeType屬性(
isElement: function(obj){ return !!(obj && obj.nodeType === 1); // 兩次感嘆號將值轉化為布爾值 }
判斷arguments對象
低版本的瀏覽器中argument對象通過Object.prototype.toString判斷后返回的是[object Object],所以需要兼容
isArguments: function(obj){ return Object.prototype.toString.call(obj) === "[object Arguments]" || (obj != null && Object.hasOwnProperty.call(obj, "callee")); }
深淺拷貝
如果是數組,實現淺拷貝,比可以slice,concat返回一個新數組的特性來實現;實現深拷貝,可以利用JSON.parse和JSON.stringify來實現,但是有一個問題,不能拷貝函數(此時拷貝后返回的數組為null)。上面的方法都屬于技巧,下面考慮怎么實現一個對象或者數組的深淺拷貝
淺拷貝
思路很簡單,遍歷對象,然后把屬性和屬性值都放在一個新的對象就OK了
var shallowCopy = function(obj) { // 只拷貝對象 if (typeof obj !== "object") return; // 根據obj的類型判斷是新建一個數組還是對象 var newObj = obj instanceof Array ? [] : {}; // 遍歷obj,并且判斷是obj的屬性才拷貝 for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; }
深拷貝
思路也很簡單,就是在拷貝的時候判斷一下屬性值的類型,如果是對象,就遞歸調用深淺拷貝函數就ok了
var deepCopy = function(obj) { if (typeof obj !== "object") return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key]; } } return newObj; }
扁平化
遞歸
循環數組元素,如果還是一個數組,就遞歸調用該方法
// 方法 1 var arr = [1, [2, [3, 4]]]; function flatten(arr) { var result = []; for (var i = 0, len = arr.length; i < len; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])) } else { result.push(arr[i]) } } return result; } console.log(flatten(arr))
toString()
如果數組的元素都是數字,可以使用該方法
// 方法2 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.toString().split(",").map(function(item){ return +item // +會使字符串發生類型轉換 }) } console.log(flatten(arr))
reduce()
// 方法3 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) } console.log(flatten(arr))
...
// 扁平化一維數組 var arr = [1, [2, [3, 4]]]; console.log([].concat(...arr)); // [1, 2, [3, 4]] // 可以扁平化多維數組 var arr = [1, [2, [3, 4]]]; function flatten(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flatten(arr))
柯里化
通用版
function curry(fn, args) { var length = fn.length; var args = args || []; return function(){ newArgs = args.concat(Array.prototype.slice.call(arguments)); if (newArgs.length < length) { return curry.call(this,fn,newArgs); }else{ return fn.apply(this,newArgs); } } } function multiFn(a, b, c) { return a * b * c; } var multi = curry(multiFn); multi(2)(3)(4); multi(2,3,4); multi(2)(3,4); multi(2,3)(4);
ES6版
const curry = (fn, arr = []) => (...args) => ( arg => arg.length === fn.length ? fn(...arg) : curry(fn, arg) )([...arr, ...args]) let curryTest=curry((a,b,c,d)=>a+b+c+d) curryTest(1,2,3)(4) //返回10 curryTest(1,2)(4)(3) //返回10 curryTest(1,2)(3,4) //返回10
防抖與節流
防抖
function debounce(fn, wait) { var timeout = null; return function() { if(timeout !== null) { clearTimeout(timeout); } timeout = setTimeout(fn, wait); } } // 處理函數 function handle() { console.log(Math.random()); } // 滾動事件 window.addEventListener("scroll", debounce(handle, 1000));
節流
var throttle = function(func, delay) { var prev = 0; return function() { var context = this; var args = arguments; var now = Date.now(); if (now - prev >= delay) { func.apply(context, args); prev = Date.now(); } } } function handle() { console.log(Math.random()); } window.addEventListener("scroll", throttle(handle, 1000));
var throttle = function(func, delay) { var timer = null; return function() { var context = this; var args = arguments; if (!timer) { timer = setTimeout(function() { func.apply(context, args); timer = null; }, delay); } } } function handle() { console.log(Math.random()); } window.addEventListener("scroll", throttle(handle, 1000));
節流中用時間戳或定時器都是可以的。更精確地,可以用時間戳+定時器,當第一次觸發事件時馬上執行事件處理函數,最后一次觸發事件后也還會執行一次事件處理函數。
var throttle = function(func, delay) { var timer = null; var startTime = 0; return function() { var curTime = Date.now(); var remaining = delay - (curTime - startTime); var context = this; var args = arguments; clearTimeout(timer); if (remaining <= 0) { func.apply(context, args); startTime = Date.now(); } else { timer = setTimeout(func, remaining); } } } function handle() { console.log(Math.random()); } window.addEventListener("scroll", throttle(handle, 1000));
模擬new
new產生的實例可以訪問Constructor里的屬性,也可以訪問到Constructor.prototype中的屬性,前者可以通過apply來實現,后者可以通過將實例的proto屬性指向構造函數的prototype來實現
我們還需要判斷返回的值是不是一個對象,如果是一個對象,我們就返回這個對象,如果沒有,我們該返回什么就返回什么
function New(){ var obj=new Object(); //取出第一個參數,就是我們要傳入的構造函數;此外因為shift會修改原數組,所以arguments會被去除第一個參數 Constructor=[].shift.call(arguments); //將obj的原型指向構造函數,這樣obj就可以訪問到構造函數原型中的屬性 obj._proto_=Constructor.prototype; //使用apply改變構造函數this的指向到新建的對象,這樣obj就可以訪問到構造函數中的屬性 var ret=Constructor.apply(obj,arguments); //要返回obj return typeof ret === "object" ? ret:obj; }
function Otaku(name,age){ this.name=name; this.age=age; this.habit="Games" } Otaku.prototype.sayYourName=function(){ console.log("I am" + this.name); } var person=objectFactory(Otaku,"Kevin","18") console.log(person.name)//Kevin console.log(person.habit)//Games console.log(person.strength)//60
模擬call
call()方法在使用一個指定的this值和若干個指定的參數值的前提下調用某個函數或方法
模擬的步驟是:將函數設為對象的屬性—>執行該函數—>刪除該函數
this參數可以傳null,當為null的時候,視為指向window
函數是可以有返回值的
簡單版
var foo = { value: 1, bar: function() { console.log(this.value) } } foo.bar() // 1
完善版
Function.prototype.call2 = function(context) { var context=context||window context.fn = this; let args = [...arguments].slice(1); let result = context.fn(...args); delete context.fn; return result; } let foo = { value: 1 } function bar(name, age) { console.log(name) console.log(age) console.log(this.value); } //表示bar函數的執行環境是foo,即bar函數里面的this代表foo,this.value相當于foo.value,然后給bar函數傳遞兩個參數 bar.call2(foo, "black", "18") // black 18 1
模擬apply
apply()的實現和call()類似,只是參數形式不同
Function.prototype.apply2 = function(context = window) { context.fn = this let result; // 判斷是否有第二個參數 if(arguments[1]) { result = context.fn(...arguments[1]) } else { result = context.fn() } delete context.fn return result }
模擬bind
Function.prototype.bind2=function(context){ var self=thisl var args=Array.prototype.slice.call(arguments,1); var fNOP=function(){}; var fBound=function(){ var bindArgs=Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindAt)) } }
模擬instanceof
function instanceOf(left,right) { let proto = left.__proto__; let prototype = right.prototype while(true) { if(proto === null) return false if(proto === prototype) return true proto = proto.__proto__; } }
模擬JSON.stringify
JSON.stringify(value[, replacer [, space]])
Boolean | Number| String 類型會自動轉換成對應的原始值。
undefined、任意函數以及symbol,會被忽略(出現在非數組對象的屬性值中時),或者被轉換成 null(出現在數組中時)。
不可枚舉的屬性會被忽略
如果一個對象的屬性值通過某種間接的方式指回該對象本身,即循環引用,屬性也會被忽略。
function jsonStringify(obj) { let type = typeof obj; if (type !== "object") { if (/string|undefined|function/.test(type)) { obj = """ + obj + """; } return String(obj); } else { let json = [] let arr = Array.isArray(obj) for (let k in obj) { let v = obj[k]; let type = typeof v; if (/string|undefined|function/.test(type)) { v = """ + v + """; } else if (type === "object") { v = jsonStringify(v); } json.push((arr ? "" : """ + k + "":") + String(v)); } return (arr ? "[" : "{") + String(json) + (arr ? "]" : "}") } } jsonStringify({x : 5}) // "{"x":5}" jsonStringify([1, "false", false]) // "[1,"false",false]" jsonStringify({b: undefined}) // "{"b":"undefined"}"
模擬JSON.parse
JSON.parse(text[, reviver])
用來解析JSON字符串,構造由字符串描述的JavaScript值或對象。提供可選的reviver函數用以在返回之前對所得到的對象執行變換(操作)。
利用eval
function jsonParse(opt) { return eval("(" + opt + ")"); } jsonParse(jsonStringify({x : 5})) // Object { x: 5} jsonParse(jsonStringify([1, "false", false])) // [1, "false", falsr] jsonParse(jsonStringify({b: undefined})) // Object { b: "undefined"}
避免在不必要的情況下使用 eval,eval() 是一個危險的函數, 他執行的代碼擁有著執行者的權利。如果你用 eval()運行的字符串代碼被惡意方(不懷好意的人)操控修改,您最終可能會在您的網頁/擴展程序的權限下,在用戶計算機上運行惡意代碼
利用new Function()
Function與eval有相同的字符串參數特性,eval 與 Function 都有著動態編譯js代碼的作用,但是在實際的編程中并不推薦使用。
var func = new Function(arg1, arg2, ..., functionBody)
var jsonStr = "{ "age": 20, "name": "jack" }" var json = (new Function("return " + jsonStr))();
創建對象
創建自定義對象最簡單的方式就是創建一個Object的實例,然后再為它添加屬性和方法,早期的開發人員經常使用這種模式來創建對象,后來對象字面量的方法成了創建對象的首選模式。雖然object構造函數或者對象字面量的方法都可以用來創建對象,但是這些方法使用同一個接口創建很多對象,會產生大量的重復代碼。為了解決這個問題,人們開始使用各種模式來創建對象,在這些模式中,一般推薦使用四種方式,包括構造函數模式、原型模式、構造函數和原型組合模式,動態原型模式,其他的方式,包括工廠模式、寄生構造函數模式、穩妥構造函數模式平時使用的較少。而這些方式中,用的最多最推薦的應是組合模式和動態原型模式
構造函數和原型組合模式
優點:
解決了原型模式對于引用對象的缺點
解決了原型模式沒有辦法傳遞參數的缺點
解決了構造函數模式不能共享方法的缺點
function Person(name) { this.name = name this.friends = ["lilei"] } Person.prototype.say = function() { console.log(this.name) } var person1 = new Person("hanmeimei") person1.say() //hanmeimei
動態原型模式
優點:
可以在初次調用構造函數的時候就完成原型對象的修改
修改能體現在所有的實例中
function Person(name) { this.name = name // 檢測say 是不是一個函數 // 實際上只在當前第一次時候沒有創建的時候在原型上添加sayName方法 //因為構造函數執行時,里面的代碼都會執行一遍,而原型有一個就行,不用每次都重復,所以僅在第一執行時生成一個原型,后面執行就不必在生成,所以就不會執行if包裹的函數, //其次為什么不能再使用字面量的寫法,我們都知道,使用構造函數其實是把new出來的對象作用域綁定在構造函數上,而字面量的寫法,會重新生成一個新對象,就切斷了兩者的聯系! if(typeof this.say != "function") { Person.prototype.say = function( alert(this.name) } }
后記:
具體的一些代碼實現請前往github項目主頁,如果大家覺得對你有幫助的話,請fork一下這個項目,搬磚不易,后期的關于JavaScript,node和框架的源代碼實現都會在github上更新,感謝你的閱讀。
參考資料:
冴羽的JavaScript專題系列
「中高級前端面試」JavaScript手寫代碼無敵秘籍
前端筆試之手寫代碼(一)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/6651.html
摘要:另外回答的時候要淡定,一些問題就算不懂也不能慌,要和面試官談笑風生,然后盡量扯回到自己懂的東西上面大公司比如百度給我的感覺就是很重視基礎思維和潛力。 —— 雖然我的offer少,但是我的拒信多啊 這幾天終于閑下來,做一點微小的工作,整理了一些之前幾家公司的前端面試題和個人經驗,想做前端的師弟妹可以參考,也歡迎各同行大神來指教~ (以下問題不分先后,時間久遠難免有些遺漏;很多問題面試官都...
摘要:另外回答的時候要淡定,一些問題就算不懂也不能慌,要和面試官談笑風生,然后盡量扯回到自己懂的東西上面大公司比如百度給我的感覺就是很重視基礎思維和潛力。 —— 雖然我的offer少,但是我的拒信多啊 這幾天終于閑下來,做一點微小的工作,整理了一些之前幾家公司的前端面試題和個人經驗,想做前端的師弟妹可以參考,也歡迎各同行大神來指教~ (以下問題不分先后,時間久遠難免有些遺漏;很多問題面試官都...
摘要:另外回答的時候要淡定,一些問題就算不懂也不能慌,要和面試官談笑風生,然后盡量扯回到自己懂的東西上面大公司比如百度給我的感覺就是很重視基礎思維和潛力。 —— 雖然我的offer少,但是我的拒信多啊 這幾天終于閑下來,做一點微小的工作,整理了一些之前幾家公司的前端面試題和個人經驗,想做前端的師弟妹可以參考,也歡迎各同行大神來指教~ (以下問題不分先后,時間久遠難免有些遺漏;很多問題面試官都...
閱讀 3723·2021-11-24 09:39
閱讀 1870·2021-11-16 11:45
閱讀 616·2021-11-16 11:45
閱讀 1029·2021-10-11 10:58
閱讀 2475·2021-09-09 11:51
閱讀 1941·2019-08-30 15:54
閱讀 687·2019-08-29 13:13
閱讀 3466·2019-08-26 12:18