(VUE!jQuery!外掛!)盤點前端群的無腦回答

lhyt發表於2018-05-05

0.前言

你是不是在前端群見過很多這種前景:這個怎麼做?怎麼拿到這些資料?怎麼更新整個列表?

回答:迴圈啊!遍歷啊!用一個陣列儲存,遍歷!jQuery!vue!

然後有一些稍微高階的:我想快一點的解決方法。我想用效能好一點的方法。

回答:遞迴啊!開個新的陣列儲存中間變數,再遍歷!document.querySelectorAll獲取全部,快取一下長度、所有的元素,遍歷!快排,小的放左邊大的放右邊,遞迴!

然後當你發現水群是解決不了的問題的時候,你已經邁出了進階的一步了。在群裡問,如果不是一堆熟人,都是陌生人的話,太簡單人家嫌無腦,太難人家嫌麻煩或者壓根不會,中等的稍微查一下資料也能做出來。所以說在一個全是陌生人的群,問不如自己動手,問了人家也可能是答非所問、要不就是暴力迴圈解決或者拿一些其他名詞裝逼(比如一些沒什麼知名度的js庫,查了一下原來是某個小功能的)。

1.我想給每一個li繫結事件,點選哪個列印相應的編號

某路人:迴圈,給每一個li綁一個事件。大概是這樣子: html:

<ul>
	<li>0</li>
	<li>1</li>
	<li>2</li>
</ul>
複製程式碼

js:

var li = document.querySelectorAll('li')
for(var i = 0;i<li.length;i++){//大約有一半的人連個length都不快取的
	li[i].onclick = function (i) {//然後順便扯一波閉包,扯一下經典面試題,再補一句let能秒殺
		return function () {
			console.log(i)
		}
	}(i)
}
複製程式碼

問題少年:那我動態新增li呢?

某路人:一樣啊,你加多少個,我就迴圈遍歷多少個

問題少年:假如我有一個按鈕,按了增加一個li,也要實現這個效果,怎麼辦

某路人:哈?一樣啊,就是在新增的時候再for迴圈重新綁事件

問題少年:...

場景還原:

html:

<ul>
	<li>0</li>
	<li>1</li>
	<li>2</li>
</ul>
<button onclick="addli()">add</button>
複製程式碼

js:

//原來的基礎上再多一個函式,然後熱心地寫下友好的註釋
function addli(){
	var newli = document.createElement('li')
	newli.innerHTML = document.querySelectorAll('li').length;//先獲取長度,把序號寫進去
	document.querySelectorAll('ul')[0].appendChild(newli)//加入
	var li = document.querySelectorAll('li')
	for(var i = 0;i<li.length;i++){//再綁一次事件
		li[i].onclick = function (i) {
			return function () {
				console.log(i)
			}
		}(i)
	}
}
複製程式碼

問題少年看見功能是實現了,但是他總感覺那麼多document是有問題,感覺瀏覽器很不舒服,而且也聽聞過“操作dom是很大開銷的”。於是他翻了一下資料,瞭解了一下物件導向、事件代理,通宵達旦寫出自己的程式碼。完事後已經是三更半夜,開啟聊天記錄,嘴角上揚地看了一下某路人的解答。問題少年看著自己親自寫的程式碼,甚是欣慰,人生邁出一小步:

html:

<input type="number" id="input">
<button onclick="addli()">add</button>
複製程式碼

js:

class Ul {
	constructor () {
		this.bindEvent = this.bindEvent()//快取合適的dom n事件
		this.ul = document.createElement('ul')
		this.bindEvent(this.ul,'click',(e)=>{
			console.log(e.target.innerHTML)
		})
		this.init = this.init()//單例模式,只能新增一個ul
		this.init(this.ul)
		this.counts = 0//li個數
	}

	bindEvent () {
		if (window.addEventListener) {
			return function (ele, event, handle, isBunble) {
				isBunble = isBunble || false
				ele.addEventListener(event, handle, isBunble)
			}
		} else if (window.attachEvent) {
			return function (ele, event, handle) {
				ele.attachEvent('on' + event, handle)
			}
		} else {
			return function (ele, event, handle) {
				ele['on' + event] = handle
			}
		}
	}

	appendLi (nodeCounts) {
		this.counts += nodeCounts
		var temp = ''
		while (nodeCounts) {
			temp += `<li>${this.counts - nodeCounts +1}</li>`
			nodeCounts --
		}
		this.ul.innerHTML += temp
	}

	init () {//單例模式
		var isappend = false
		return function (node) {
			if(!isappend){
				document.body.appendChild(node)
				isappend = true
			}
		}
		
	}
}
var ul = new Ul()
function addli(){
	if(!input.value){
		ul.appendLi(1)
	}else{
		ul.appendLi(+input.value)
		input.value = ''
	}
}
複製程式碼

2. 我想讓幾個div類似於checkbox那種效果

也就是,幾個div,點哪個哪個亮,點另一個,正在亮著的div馬上暗,新點選的那個亮起來。

某路人:每一個div有兩個類,click類表示被點。每次點一個div,迴圈遍歷全部div重置狀態為test類,然後把被點的那個變成click。於是乎,很快寫出了一段程式碼

html:

<script src="https://cdn.bootcss.com/jquery/3.2.1/jquery.min.js"></script>
<div class="test"></div>
<div class="test"></div>
<div class="test"></div>
複製程式碼

css:

		.test{
			width: 100px;
			height: 100px;
			background-color: #0f0;
			margin: 10px
		}
		.click{
			background-color: #f00
		}
複製程式碼

js:

var divs = $('.test')
divs.click(function(){
	divs.each(function(){
		this.className = 'test'
	})
	this.className =  'click'
})
複製程式碼

問題少年反問:我不知道什麼是jQuery,但是我覺得應該這樣子寫

var divs = document.querySelectorAll('.test')
window.onclick = (function () {
	var pre
	return function (e) {
		if(pre !== undefined){
			divs[pre].className = 'test'
		}
		pre = Array.prototype.indexOf.call(divs,e.target)
		e.target.className = 'click'
	}
})()
複製程式碼

某路人:我不知道你綁個事件搞個IIFE幹啥,而且我建議你去學一下jQuery

問題少年(心想):明明console測試的執行時間,他的平均在0.26ms左右,我這個方法就是0.12ms左右,為什麼一定要用jQuery?不管了,滾回去繼續學習。

3. 從1000到5000中取出全部每一位數字的和為5的數

問題少年:rt,求一個快一點的方法

路人甲:

Array(4000).fill(1001).map((v,i)=>v+i).filter(n=>(n+"").split("").reduce(((s,x)=>+x+s),0)==5)
複製程式碼

吃瓜群眾:哇,大神,方法太簡單了,通俗易懂

路人乙:

let temp = []
for(let i = 1000;i<=5000;i++){
  temp.push(i.toString().split('').map(x=>+x).reduce((a,b)=>a+b,0) === 5&&i)
}
console.log(temp.filter(x=>x!==false))
複製程式碼

吃瓜群眾:什麼時候才能寫出像大神那樣優美的程式碼

路人:其實也不算什麼,完全沒有考慮演算法複雜度,也沒有做優化

道高一尺魔高一丈,接著又來了一種通用的方法:

var f = (s, l) => --l ? Array(s+1).fill(0).map((_,i)=> f(s-i,l).map(v =>i+v)).reduce((x,s)=>[...s,...x],[]):[[s]];
f(5, 4).filter(s=>s[0]>0)
複製程式碼

估計問題少年也沒有完全滿足,雖然答案是簡短的es6和api的靈活運用,但是方法還是有點簡單無腦,做了多餘的迴圈。

稍微優化一下的版本,迴圈次數只是縮減到600多次:(拋開ES6裝逼的光環)

function f (_from, _to, _n) {
	var arr = []
	var _to = _to + ''
	var len = _to.length
	!function q (n, str) {
		if( str.length === len) {//保證4位數
			if(str.split('').reduce((s,x)=>+x+s,0) == _n && str[0] != '0'){//每一位加起來等於5
				arr.push(+str)
			}
				return
		}
		for(var i = 0;i <= _n - n;i++){//分別是0、1、2、3、4、5開頭的
			q(i,i + '' + str)//一位一位地遞迴
		}
	}(~~_from/1000,'')
	return arr
}
f (1000, 5000,5)
複製程式碼

可讀性顯然差了很多,但是執行的時間就差太多了:

  • 一行ES6: 16.3310546875ms
  • for迴圈: 40.275146484375ms
  • 優化版本: 1.171826171875ms

4. 我從介面拿到了返回的json資料,但是我又要操作這個資料而且不能汙染原資料

某路人不加思考秒回:var data2 = data

另外一個路人稍加思考:不是吧,你先要深拷貝一下

問題少年:那怎麼深拷貝呢

某路人:JSON.stringify再JSON.parse

問題少年:謝啦,真好用

稍微瞭解的人都知道,一個stringify並不能解決所有的深拷問題。第二天,問題少年又回來了:我用了轉義來拷貝,老闆說要弄死我,現在專案裡面所有的物件導向都炸了

某路人:臥槽,我就不信還有什麼是stringify和parse解決不了的

另一個路人:原型鏈斷了、undefined變成空值、環引用出錯,用了物件導向的全都泡湯了

問題少年特別無助,也沒有人出來幫忙,也許上天有一個稍微好一點點的參考答案,等著他發掘:

function copy (arr) {
	var temp
	if (typeof arr === 'number'||typeof arr === 'boolean'||typeof arr === 'string') {
		return arr
	}
	if(Object.prototype.toString.call(arr) === "[object Array]" ){
		temp = []
		for(x in arr){
			temp[x] = copy(arr[x])
		}
		return temp
	}else if(Object.prototype.toString.call(arr) === "[object RegExp]"){
		temp = arr.valueOf()
		var str = (temp.global ? 'g' : '') +(temp.ignoreCase ? 'i': '')+(temp.multiline ? 'm' : '')
		return new RegExp(arr.valueOf().source,str)
	}else if(Object.prototype.toString.call(arr) === "[object Function]"){
		var str = arr.toString();
		/^function\s*\w*\s*\(.*\)\s*\{(.*)/m.test(str);
		var str1 = RegExp.$1.slice(0,-1);
		return new Function(str1)//函式有換行就出事,求更好的解決方法
	}else if(Object.prototype.toString.call(arr) === "[object Date]"){
		return new Date(arr.valueOf());
	}else if(Object.prototype.toString.call(arr) === "[object Object]"){
		try{
			temp = JSON.parse(JSON.stringify(arr))
		}catch(e){//環引用解決:取出環引用部分stringify再放回去
			var temp1 = {},circle,result,reset = false,hash
			function traverse (obj) {
				for(x in obj){
					if(!reset&&obj.hasOwnProperty(x)){
						if(!temp1[x]){
							temp1[x] = obj[x]
						}else if(typeof obj[x] === 'object'&&typeof temp1[x] === 'object'){
							try{
								JSON.stringify(obj[x])
							}catch(e){
								circle = obj[x]
								hash = new Date().getTime()
								obj[x] = hash
								break
							}finally{
								return traverse(obj[x])
							}		
						}
						if(typeof obj[x] === 'object'){
							return traverse(obj[x])
						}
					}else if(reset){
						if(obj[x] === hash){
							obj[x] = circle
							return
						}
						if(typeof obj[x] === 'object'){
							return traverse(obj[x])
						}
					}
				}
			}
			traverse(arr)
			result = JSON.parse(JSON.stringify(arr))
			reset = true
			traverse(result)
			traverse(arr)
			temp = result	
		}finally{//考慮到原型鏈和Object.create(null)
			if(arr.__proto__.constructor && arr.__proto__.constructor !== Object){
				temp.__proto__.constructor = arr.__proto__.constructor
			}
			if(!arr.__proto__.constructor){
                                temp.__proto__.constructor = null
                        }
			return temp
		}
	}
	if(!arr){
		return arr
	}
}
複製程式碼

5.在陣列內動態新增元素,打鉤的求和,求助啊

給出的圖片大概是這樣子,選取某個li就把他的價格算入sum裡面:

image

相信50%的人都會這樣子,某路人:vue啊,v-for顯示,computed

甚至有的人2分鐘把程式碼擼出來了:

html:

<script src="https://cdn.bootcss.com/vue/2.5.13/vue.min.js"></script>
<div id='app'>
      <ul>
      	<li v-for="x in list">
      		{{x.price}}
      		<input type="checkbox"  v-model="x.selected">
      	</li>
      </ul>
      <button @click="add">add</button>
      {{sum}}
</div>  
複製程式碼

js:

new Vue({
		el:'#app',
		data(){
			return {
				list:[{price:1,selected:false},{price:2,selected:false},{price:3,selected:false}]
			}
		},
		methods:{
			add(){
				this.list.push({price:1,selected:false})
			}
		},
		computed:{
			sum(){
				return this.list.reduce((s,x)=>x.selected?+x.price+s:s,0)
			}
		})
複製程式碼

問題少年:我們專案不用vue

頓時炸開了:臥槽,居然原生。不用vue你怎麼寫?要不你把這個模組改vue的……

問題少年,我們專案這個模組就用物件導向,工具庫偶爾用用lodash

又炸開了:lodash?什麼鬼?表示看不懂。本菜雞繼續潛水

廣告一波,lodash深拷貝,目前算是最完美的深拷貝

那我們繼續,然後問題少年就問了一下vue的問題,接著就潛了

於是可以猜到問題少年想寫的程式碼:

html:

<ul id="app"></ul>
複製程式碼

js:

app.addEventListener('change',(function(){
	var lastpick = 0
	return function (e) {//相信很多人是每次change後,用迴圈一個個加起來的
		lastpick += e.target.checked?+e.target.value:-e.target.value
		res.innerHTML = lastpick
	}
})())
var list = [{price:1,selected:false},{price:2,selected:false},{price:3,selected:false}]
var append = ''
for(var i = 0;i<list.length;i++){
	append += '<li><input type="checkbox" value='+list[i].price+'>'+list[i].price+'</li>'
}
app.innerHTML = append
複製程式碼

6. 後端不幫我分頁,前端分頁怎麼容易一點

問題少年:是個人中心來的,資料不多,而且使用者一般都會一頁頁去瀏覽全部資料的,因為這些消費資料必須全部看一遍才能瞭解到情況

路人甲:拿到全部資料後,根據每頁資料和第幾頁for迴圈取出元素並插入相應的html,每次切換也遍歷一次

路人乙:臥槽,這後端吃屎的吧,居然不分頁

路人丙:當然是按需載入啊,你不應該一下載入完全部

路人丁:用jQuery分頁外掛啊

於是勤勞熱心愛解答問題的群友馬上寫出了答案:

html:

      每頁資料<select id="app">
      	<option>2</option>
      	<option>3</option>
      	<option>4</option>
      </select>
      <div>
      	<button onclick="pre()">上一頁</button>
      	<button onclick="next()">下一頁</button>
      	<p>當前是<span id="current"></span></p>
      		<table>
      			<tbody id="content">
      			</tbody>
      		</table>
      </div>
複製程式碼

js:

var data = [1,2,3,4,5,6,7,8,9,10,11,12,13]//後端拿到的資料
var currentIndex = 1
var per = 2
app.onchange = function (e) {
	per = e.target.value
	update(1)//更改每頁資料時候返回第一頁
}
function pre() {
	currentIndex = currentIndex > 1?currentIndex-1:currentIndex
	update(currentIndex,per)
}
function next(){
	currentIndex = currentIndex < Math.floor(data.length/per)+1?currentIndex+1:currentIndex
	update(currentIndex,per)
}
window.onload = function () {
	update()
}
function update(currentPage, perPage){
	var currentPage = currentPage ||currentIndex
	var perPage = perPage || per
	var ctx = ''
	for(var i = perPage*(currentPage-1);i<perPage*currentPage;i++){
		if(data[i]!==undefined) ctx += '<tr><td>'+data[i]+'</td></tr>'
	}
	content.innerHTML = ctx
	current.innerHTML = currentPage
}
複製程式碼

當然,後端不幫忙分頁的情況下,通常是資料量是不大的,上面的做法也是沒什麼問題。但是,在這個場景下,使用者需要瀏覽器所有的資料,所以不存在按需分頁。我們不應該在使用者需要的時候才迴圈的,這樣子對於使用者,時間複雜度是n。如果是先分頁再直接拿出來,複雜度就是1。這個場景下,他是全部都瀏覽的,所以我們先分頁再取資料。 改進的js:

var data = [1,2,3,4,5,6,7,8,9,10,11,12,13]

var currentIndex = 1
var per = 2
var list = []
app.onchange = function (e) {
	per = +e.target.value
	getdatalist(per)
	update(1)
}
function pre() {
	console.time('pre')
	currentIndex = currentIndex>1?currentIndex-1:currentIndex
	update(currentIndex, per)
	console.timeEnd('pre')
}
function next(){
	console.time('next')
	currentIndex = currentIndex < Math.floor(data.length/per)+1?currentIndex+1:currentIndex
	update(currentIndex, per)
	console.timeEnd('next')
}
window.onload = function () {
	getdatalist(2)
	update()
}
function getdatalist (per) {
	var per = per || 2
	list = []
	var pages = ~~(data.length/per)+1
	for(var i = 0;i<pages;i++){
		var temp = ''
		data.slice(i*per,i*per+per).forEach(function(item){
			temp += '<tr><td>'+item+'</td></tr>'
		})
		list.push(temp)
	}
}
function update(currentPage, perPage){
	currentIndex = currentPage || 1
	var perPage = perPage || per
	content.innerHTML = list[currentIndex-1]
	current.innerHTML = currentIndex
}
複製程式碼

時間測試:

使用者切換頁面的時候時間消耗

按需迴圈:

image

提前分頁:

image

隨著分頁越來越多,提前分頁在切換的時間上的優勢越來越大。當然,正常的情況下使用者一般都不會把全部資料都瀏覽完的,所以一般也是用按需分頁更好。

7.我們要做一個抽獎活動,需要使用者的號碼存在兩個數和為100算中獎

問題少年:隨機數字分佈得比較均勻(但是亂序),比如3、2、1、4、5、7,而不是5、1、6、7、8

路人甲:一個個迴圈,再判斷

let arr =[12,40,60,80,62,13,58,87]
let obj = {}
arr.map(item=>{
	if(!obj[item]){
		let val = 100-item
		obj[item] = 1
		if(arr.includes(val)){
			obj[val] = 1
			console.log(item,val)
		}
	}
})
複製程式碼

於是,馬上有人喊666了。可是,這ES6用得有點浪費了。問題少年貌似不滿足,說:我們的資料量可能有一點大。

於是,有一個雙指標的版本:

function f(arr,res){
	var l = arr.length
	var p = ~~(l/2)
	var i = p - 1
	var j = p 
	var ishasresult = false
arr = arr.sort((a,b)=>a-b)
	while(i >= 0 && j < l){
    	if(arr[i] + arr[j] > res){
    		i -- 
    	}else if(arr[i] + arr[j] < res){
    		j ++
    	}else{
    		ishasresult = true
    		break
    	}
    }
    return ishasresult
}
複製程式碼

假設排序是快排,總的時間複雜度就是nlogn+logn

其實,用ES6的話,這樣子更快:

var myset = new Set(arr);
arr.find(function(value, index, arr) {
  return myset.has(100 - value);
})
複製程式碼

時間測試:

無腦ES6迴圈:2.419189453125ms 2.35595703125ms 1.330078125ms

雙指標: 0.0400390625ms 0.0283203125ms 0.0390625ms

簡化ES6:0.1240234375ms 0.10986328125ms 0.092041015625ms

8. 陣列向頭部新增元素,concat和一個個unshift那個效率高

路人甲:unshift,畢竟它是專門在頭部新增的,concat是連線陣列的,演算法肯定比unshift複雜,es6的…算是淘汰了concat了吧

測試結果:

image

很明顯的,unshift和...是一個個遍歷,concat就是直接連起來複雜度是1。但是還是需要看實際場景的,沒有絕對的淘汰、取代的這種說法。

總結

是不是道出了一些熟悉的經歷?迴圈啊、遍歷,再xxx、用vue啊、用jQuery的xx、用xx外掛。對於問問題的人,要先表示清楚需求,儘量講詳細一點,而不是隨便截個圖就問,能百度能靠文件的,也沒有必要問。如果是有意義的問題,那麼大家就得好好思考,瞭解人家的應用場景,而不是無腦迴圈,也不是直接拋一個xx外掛、xx.js給人家,因為人家也懂的,只是想要一個更好的答案或者不是一個無腦的答案。當然,純小白的話就算了。

如果是大牛,也許朋友圈子裡面沒有這種事情,也沒有進這種群。我也是學了半年的菜鳥,很多應用場景的經驗不足。但是有的人,工作了幾年還寫出一些無腦的程式碼,程式碼中又暴露了各種細節沒怎麼處理。不是說我覺得他們不如我,我只是感覺,這可能就是有一些人工作10年1年經驗的原因。

另外,面試的時候,是不是經常被問閉包拿來幹什麼的,上面例子有幾個經典的閉包應用,所以面試的時候不要只是說出閉包是什麼、閉包會記憶體洩漏,也要知道,閉包用於幹什麼,無非就是快取、柯里化、單例模式、模組化,而且能保護內部變數。那麼,也問一下自己,究竟有沒有用過閉包來幹一些有意義的事情,有沒有說過 ‘無緣無故搞個IIFE有什麼用’ 這種話?你的ES6僅僅是不是拿來簡寫或者追求視覺上的程式碼簡短來裝逼的呢,有沒有把proxy玩得飛起,解構賦值用得多嗎,有沒有知道不能捕獲非同步的try可以靠async+await捕捉非同步,set用過最多的是不是去重,常用的除了let、const、promise、async-await、...、set、反引號+字串模板之外是不是沒有其他的了?

轉載時請註明出處,來自lhyt的掘金

相關文章