前端基本功-示例代碼 (一) 點這裏
前端基本功-示例代碼 (二) 點這裏
1.一像素
僞類 + transform 實現
對於老項目,有沒有什麼辦法能兼容1px的尷尬問題了,個人認爲僞類+transform是比較完美的方法了。
原理是把原先元素的 border 去掉,然後利用 :before 或者 :after 重做 border ,並 transform 的 scale 縮小一半,原先的元素相對定位,新做的 border 絕對定位。
單條border樣式設置:
.scale-1px{
position: relative;
border:none;
}
.scale-1px:after{
content: '';
position: absolute;
bottom: 0;
background: #000;
width: 100%;
height: 1px;
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
-webkit-transform-origin: 0 0;
transform-origin: 0 0;
}
四條boder樣式設置:
.scale-1px{
position: relative;
margin-bottom: 20px;
border:none;
}
.scale-1px:after{
content: '';
position: absolute;
top: 0;
left: 0;
border: 1px solid #000;
-webkit-box-sizing: border-box;
box-sizing: border-box;
width: 200%;
height: 200%;
-webkit-transform: scale(0.5);
transform: scale(0.5);
-webkit-transform-origin: left top;
transform-origin: left top;
}
最好在使用前也判斷一下,結合 JS 代碼,判斷是否 Retina 屏:
if(window.devicePixelRatio && devicePixelRatio >= 2){
document.querySelector('ul').className = 'scale-1px';
}
方法二
/*移動端正常展示1px的問題 start*/
%border-1px{
display: block;
position:absolute;
left: 0;
width: 100%;
content: ' ';
}
.border-1px{
position: relative;
&::after{
@extend %border-1px;
bottom: 0;
border-top: 1px solid #ccc;
}
&::before{
@extend %border-1px;
top: 0;
border-bottom: 1px solid #ccc;
}
}
@media (-webkit-min-device-pixel-ratio:1.5),(min-device-pixel-ratio:1.5){
.border-1px{
&::after{
-webkit-transform: scaleY(0.7);
transform: scaleY(0.7);
}
}
}
@media (-webkit-min-device-pixel-ratio:2),(min-device-pixel-ratio:2){
.border-1px{
&::after{
-webkit-transform: scaleY(0.5);
transform: scaleY(0.5);
}
}
}
/*移動端正常展示1px的問題 end*/
方法三
.hairline-border {
box-shadow: 0 0 0 1px;
}
@media (min-resolution: 2dppx) {
.hairline-border {
box-shadow: 0 0 0 0.5px red;
}
}
@media (min-resolution: 3dppx) {
.hairline-border {
box-shadow: 0 0 0 0.33333333px;
}
}
@media (min-resolution: 4dppx) {
.hairline-border {
box-shadow: 0 0 0 0.25px;
}
}
2.動畫
animation:mymove 5s infinite;
@keyframes mymove {
from {top:0px;}
to {top:200px;}
}
js實現一個持續的動畫效果
//兼容性處理
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function(callback){
window.setTimeout(callback, 1000 / 60);
};
})();
var e = document.getElementById("e");
var flag = true;
var left = 0;
function render() {
left == 0 ? flag = true : left == 100 ? flag = false : '';
flag ? e.style.left = ` ${left++}px` :
e.style.left = ` ${left--}px`;
}
(function animloop() {
render();
requestAnimFrame(animloop);
})();
3. 實現sum(2)(3)
// 寫一個 function 讓下面兩行代碼輸出的結果都爲 5
console.log(sum(2, 3));
console.log(sum(2)(3));
實現
function sum() {
var cache;
if (arguments.length === 1) {
cache = arguments[0];
return function ( number ) {return cache + number;};
}
else return arguments[0] + arguments[1];
};
4. 函數柯里化
函數柯里化指的是將能夠接收多個參數的函數轉化爲接收單一參數的函數,並且返回接收餘下參數或結果的新函數的技術。
函數柯里化的主要作用和特點就是參數複用、提前返回和延遲計算/執行。
1. 參數複用
引導
// 普通函數
function add(x,y){
return x + y;
}
add(3,4); //5
// 實現了柯里化的函數
// 接收參數,返回新函數,把參數傳給新函數使用,最後求值
let add = function(x){
return function(y){
return x + y;
}
};
add(3)(4); // 7
通用的柯里化函數
感覺currying就是返回函數的函數,在此函數閉包中定義了私有域變量。
function curry(fn) {
let slice = Array.prototype.slice, // 將slice緩存起來
args = slice.call(arguments, 1); // 這裏將arguments轉成數組並保存
return function() {
// 將新舊的參數拼接起來
let newArgs = args.concat(slice.call(arguments));
return fn.apply(null, newArgs); // 返回執行的fn並傳遞最新的參數
}
}
if (typeof Function.prototype.bind === "undefined"){
Function.prototype.bind = function (thisArgs){
var fn = this,
slice = Array.prototype.slice,
args = slice.call(arguments, 1);
return function (){
let newArgs = args.concat(slice.call(arguments))
return fn.apply(thisArgs, newArgs);
}
}
}
ES6版的柯里化函數
function curry(fn, ...allArgs) {
const g = (...allArgs) => allArgs.length >= fn.length ?
fn(...allArgs) : (...args) => g(...allArgs, ...args)
return g;
}
// 測試用例
const foo = curry((a, b, c, d) => {
console.log(a, b, c, d);
});
foo(1)(2)(3)(4); // 1 2 3 4
const f = foo(1)(2)(3);
f(5); // 1 2 3 5
function trueCurrying(fn, ...args) {
if (args.length >= fn.length) {
return fn(...args)
}
return function (...args2) {
return trueCurrying(fn, ...args, ...args2)
}
}
// 比較多次接受的參數總數與函數定義時的入參數量,
//當接受參數的數量大於或等於被 Currying 函數的傳入參數數量時,
//就返回計算結果,否則返回一個繼續接受參數的函數。
//注意這點和上邊的區別
題目:需要寫一個函數,滿足
curry(fn)(1)(2)(3) //6
var fn = function(a,b,c) {
return a+b+c;
}
function curry(fn) {
var arr = [],
mySlice = arr.slice
fnLen = fn.length;
function curring() {
arr = arr.concat(mySlice.call(arguments));
if(arr.length < fnLen) {
return curring;
}
return fn.apply(this, arr);
}
return curring;
}
curry(fn)(1)(2)(3);//6
本小題來自:幾個讓我印象深刻的面試題(一)
2. 提前返回
var addEvent = function(el, type, fn, capture) {
if (window.addEventListener) {
el.addEventListener(type, function(e) {
fn.call(el, e);
}, capture);
} else if (window.attachEvent) {
el.attachEvent("on" + type, function(e) {
fn.call(el, e);
});
}
};
上面的方法有什麼問題呢?很顯然,我們每次使用addEvent爲元素添加事件的時候,(eg. IE6/IE7)都會走一遍if...else if ...,其實只要一次判定就可以了,怎麼做?–柯里化。改爲下面這樣子的代碼:
var addEvent = (function(){
if (window.addEventListener) {
return function(el, sType, fn, capture) {
el.addEventListener(sType, function(e) {
fn.call(el, e);
}, (capture));
};
} else if (window.attachEvent) {
return function(el, sType, fn, capture) {
el.attachEvent("on" + sType, function(e) {
fn.call(el, e);
});
};
}
})();
初始addEvent的執行其實值實現了部分的應用(只有一次的if...else if...判定),而剩餘的參數應用都是其返回函數實現的,典型的柯里化。
對比:惰性加載
let addEvent = function(ele, type, fn) {
if (window.addEventListener) {
addEvent = function(ele, type, fn) {
ele.addEventListener(type, fn, false);
}
} else if (window.attachEvent) {
addEvent = function(ele, type, fn) {
ele.attachEvent('on' + type, function() {
fn.call(ele)
});
}
}
addEvent(ele, type, fn);
3. 延遲計算/運行
ES5中的bind方法
if (!Function.prototype.bind) {
Function.prototype.bind = function(context) {
var self = this,
args = Array.prototype.slice.call(arguments);
return function() {
return self.apply(context, args.slice(1));
}
};
}
推薦閱讀:從一道面試題認識函數柯里化
參考文章:ES6版的柯里化函數、JS中的柯里化(currying)
5.手寫一個 bind 方法
帶一個參數:
Function.prototype.bind = function(context) {
let self = this,
slice = Array.prototype.slice,
args = slice.call(arguments);
return function() {
return self.apply(context, args.slice(1));
}
};
帶多個參數:
//ES3實現
if(!Function.prototype.bind){
Function.prototype.bind = function(o, args){
var self = this,
boundArgs = arguments;//注:arguments是指sum.bind(null,1)中的參數null和1
return function(){ //此時返回的只是一個函數
var args = [], i;
for(var i=1; i< boundArgs.length; i++){
args.push(boundArgs[i]);
}
for(var i =0; i< arguments.length; i++){
args.push(arguments[i]);//注:這裏的arguments是指result(2)中的參數2
}
return self.apply(o, args);
}
}
}
或者
// 代碼來自書籍 《javaScript 模式》
if (typeof Function.prototype.bind === "undefined"){
Function.prototype.bind = function (thisArgs){
var fn = this,
slice = Array.prototype.slice,
args = slice.call(arguments, 1);
return function (){
return fn.apply(thisArgs, args.concat(slice.call(arguments)));
}
}
}
//注:前後arguments不是一回事哦~
//調用
var sum = function(x,y){ return x+y };
var result = sum.bind(null,1);
result(2); // 3
或者
Function.prototype.bind = function(){
var fn = this;
var args = Array.prototye.slice.call(arguments);
var context = args.shift();
return function(){
return fn.apply(context, args.concat(Array.prototype.slice.call(arguments)));
};
本節參考文章:js中的bind
其他文章:JavaScirpt 的 bind 函數究竟做了哪些事
6.經典面試問題:new 的過程
首先來看一下,函數聲明的過程
// 實際代碼
function fn1() {}
// JavaScript 自動執行
fn1.protptype = {
constructor: fn1,
__proto__: Object.prototype
}
fn1.__proto__ = Function.prototype
var a = new myFunction("Li","Cherry");
//僞代碼
new myFunction{
var obj = {};
obj.__proto__ = myFunction.prototype;
var result = myFunction.call(obj,"Li","Cherry");
return typeof result === 'object'? result : obj;
}
- 創建一個空對象 obj;
- 將新創建的空對象的隱式原型指向其構造函數的顯示原型。
- 使用 call 改變 this 的指向
- 如果無返回值或者返回一個非對象值,則將 obj 返回作爲新對象;如果返回值是一個新對象的話那麼直接直接返回該對象。
所以我們可以看到,在 new 的過程中,我們是使用 call 改變了 this 的指向。
7.javascript裏面的繼承怎麼實現,如何避免原型鏈上面的對象共享
什麼是原型鏈
當一個引用類型繼承另一個引用類型的屬性和方法時候就會產生一個原型連。
ES5:寄生組合式繼承:通過借用構造函數來繼承屬性和原型鏈來實現子繼承父。
function ParentClass(name) {
this.name = name;
}
ParentClass.prototype.sayHello = function () {
console.log("I'm parent!" + this.name);
}
function SubClass(name, age) {
//若是要多個參數可以用apply 結合 ...解構
ParentClass.call(this, name);
this.age = age;
}
SubClass.prototype.sayChildHello = function (name) {
console.log("I'm child " + this.name)
}
SubClass.prototype = Object.create(ParentClass.prototype);
SubClass.prototype.constructor = SubClass;
let testA = new SubClass('CRPER')
// Object.create()的polyfill
/*
function pureObject(obj){
//定義了一個臨時構造函數
function F() {}
//將這個臨時構造函數的原型指向了傳入進來的對象。
F.prototype = obj;
//返回這個構造函數的一個實例。該實例擁有obj的所有屬性和方法。
//因爲該實例的原型是obj對象。
return new F();
}
*/
或
function subClass() {
superClass.apply(this, arguments);
this.abc = 1;
}
function inherits(subClass, superClass) {
function Inner() {}
Inner.prototype = superClass.prototype;
subClass.prototype = new Inner();
subClass.prototype.constructor = subClass;
}
inherits(subClass, superClass);
subClass.prototype.getTest = function() {
console.log("hello")
};
ES6: 其實就是ES5的語法糖,不過可讀性很強..
class ParentClass {
constructor(name) {
this.name = name;
}
sayHello() {
console.log("I'm parent!" + this.name);
}
}
class SubClass extends ParentClass {
constructor(name) {
super(name);
}
sayChildHello() {
console.log("I'm child " + this.name)
}
// 重新聲明父類同名方法會覆寫,ES5的話就是直接操作自己的原型鏈上
sayHello(){
console.log("override parent method !,I'm sayHello Method")
}
}
let testA = new SubClass('CRPER')
8.繼承 JS 內置對象(Date)
寫在前面,本節只記錄了如何繼承Date對象...的解決方案,具體問題和解析過程請看原文
ES5
// 需要考慮polyfill情況
Object.setPrototypeOf = Object.setPrototypeOf ||
function(obj, proto) {
obj.__proto__ = proto;
return obj;
};
/**
* 用了點技巧的繼承,實際上返回的是Date對象
*/
function MyDate() {
// bind屬於Function.prototype,接收的參數是:object, param1, params2...
var dateInst = new(Function.prototype.bind.apply(Date, [Date].concat(Array.prototype.slice.call(arguments))))();
// 更改原型指向,否則無法調用MyDate原型上的方法
// ES6方案中,這裏就是[[prototype]]這個隱式原型對象,在沒有標準以前就是__proto__
Object.setPrototypeOf(dateInst, MyDate.prototype);
dateInst.abc = 1;
return dateInst;
}
// 原型重新指回Date,否則根本無法算是繼承
Object.setPrototypeOf(MyDate.prototype, Date.prototype);
MyDate.prototype.getTest = function getTest() {
return this.getTime();
};
let date = new MyDate();
// 正常輸出,譬如1515638988725
console.log(date.getTest());
ES6
class MyDate extends Date {
constructor() {
super();
this.abc = 1;
}
getTest() {
return this.getTime();
}
}
let date = new MyDate();
// 正常輸出,譬如1515638988725
console.log(date.getTest());
注意:這裏的正常輸出環境是直接用ES6運行,不經過babel打包,打包後實質上是轉化成ES5的,所以效果完全不一樣,會報錯的
9.簡易雙向數據綁定
<body>
<input type="text" id="foo">
<p id="test"></p>
<script>
var user = {}
Object.defineProperty(user, 'inputValue', {
configurable: true,
get: function() {
return document.getElementById('foo').value
},
set: function(value) {
document.getElementById('foo').value = value
document.getElementById('test').innerHTML = value
}
})
document.getElementById('foo').addEventListener('keyup', function() {
document.getElementById('test').innerHTML = user.inputValue
})
</script>
</body>
10.JavaScript實現發佈-訂閱模式
發佈-訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知。JavaScript開發中我們一般用事件模型來代替傳統的發佈-訂閱模式
示例1
function Dep() {//發佈者
this.subs = [];
}
Dep.prototype.addSub = function (sub) {
this.subs.push(sub);
}
Dep.prototype.notify = function () {
this.subs.forEach(sub=>sub.update());
}
function Watcher(fn) {//訂閱者
this.fn = fn;
}
Watcher.prototype.update = function () {
this.fn();
}
var dep = new Dep();
dep.addSub(new Watcher(function () {
console.log('okokok');
}))
dep.notify();
示例2
function Event(){
this.list={},
this.on=function(key,cb){//訂閱事件
if(!this.list[key]){
this.list[key] = []
}
this.list[key].push(cb)
},
this.emit = function(){//觸發事件
var key = Array.prototype.shift.call(arguments)
var e = this.list[key]
if(!e){
return
}
var args = Array.prototype.slice.call(arguments)
for(var i = 0;i<e.length;i++){
e[i].apply(null,args)
}
}
}
嘗試一下:
var a = new Event()
a.on('a',function(x){console.log(x)})
a.emit('a',1)//1
推薦閱讀:從單向到雙向數據綁定
示例3
var myBus = (function() {
var clienlist = {},
addlisten, trigger, remove;
/**
* 增加訂閱者
* @key {String} 類型
* @fn {Function} 回掉函數
* */
addlisten = function(key, fn) {
if(!clienlist[key]) {
clienlist[key] = [];
}
clienlist[key].push(fn);
};
/**
* 發佈消息
* */
trigger = function() {
var key = [].shift.call(arguments), //取出消息類型
fns = clienlist[key]; //取出該類型的對應的消息集合
if(!fns || fns.length === 0) {
return false;
}
for(var i = 0, fn; fn = fns[i++];) {
fn.apply(this, arguments);
}
};
/**
* 刪除訂閱
* @key {String} 類型
* @fn {Function} 回掉函數
* */
remove = function(key, fn) {
var fns = clienlist[key]; //取出該類型的對應的消息集合
if(!fns) { //如果對應的key沒有訂閱直接返回
return false;
}
if(!fn) { //如果沒有傳入具體的回掉,則表示需要取消所有訂閱
fns && (fns.length = 0);
} else {
for(var l = fns.length - 1; l >= 0; l--) { //遍歷回掉函數列表
if(fn === fns[l]) {
fns.splice(l, 1); //刪除訂閱者的回掉
}
}
}
};
return {
$on: addlisten,
$emit: trigger,
$off: remove
}
})();
示例4
這個示例更像示例2、示例3的總結,我也放這裏吧,多看幾種寫法也多少開闊一下思路或全當複習
賣燒餅的店主可以把小明、小龍的電話記錄下來,等店裏有燒餅了在通知小龍小明來拿這就是所謂的發佈-訂閱模式,代碼如下:
/*燒餅店*/
var Sesamecakeshop={
clienlist:[],//緩存列表
addlisten:function(fn){//增加訂閱者
this.clienlist.push(fn);
},
trigger:function(){//發佈消息
for(var i=0,fn;fn=this.clienlist[i++];){
fn.apply(this,arguments);
}
}
}
/*小明發布訂閱*/
Sesamecakeshop.addlisten(function(price,taste){
console.log("小明發布的"+price+"元,"+taste+"味道的");
});
/*小龍發佈訂閱*/
Sesamecakeshop.addlisten(function(price,taste){
console.log("小龍發佈的"+price+"元,"+taste+"味道的");
});
Sesamecakeshop.trigger(10,"椒鹽");
從代碼中可以看出,只有小明,小龍預定了燒餅,燒餅店就可以發佈消息告訴小龍與小明。但是有個問題不知道大家發現了沒有。小明只喜歡椒鹽味道的。而小龍只喜歡焦糖味道的。上面的代碼就滿足不了客戶的需求,給客戶一種感覺就是,不管我喜歡不喜歡,你都會發給我。如果發佈比較多,客戶就會感到厭煩,甚至會想刪除訂閱。下邊是對代碼進行改良大家可以看看。
/*燒餅店*/
var Sesamecakeshop={
clienlist:{},/*緩存列表*/
/**
* 增加訂閱者
* @key {String} 類型
* @fn {Function} 回掉函數
* */
addlisten:function(key,fn){
if(!this.clienlist[key]){
this.clienlist[key]=[];
}
this.clienlist[key].push(fn);
},
/**
* 發佈消息
* */
trigger:function(){
var key=[].shift.call(arguments),//取出消息類型
fns=this.clienlist[key];//取出該類型的對應的消息集合
if(!fns || fns.length===0){
return false;
}
for(var i=0,fn;fn=fns[i++];){
fn.apply(this,arguments);
}
},
/**
* 刪除訂閱
* @key {String} 類型
* @fn {Function} 回掉函數
* */
remove:function(key,fn){
var fns=this.clienlist[key];//取出該類型的對應的消息集合
if(!fns){//如果對應的key沒有訂閱直接返回
return false;
}
if(!fn){//如果沒有傳入具體的回掉,則表示需要取消所有訂閱
fns && (fns.length=0);
}else{
for(var l=fns.length-1;l>=0;l--){//遍歷回掉函數列表
if(fn===fns[l]){
fns.splice(l,1);//刪除訂閱者的回掉
}
}
}
}
}
/*小明發布訂閱*/
Sesamecakeshop.addlisten("焦糖",fn1=function(price,taste){
console.log("小明發布的"+price+"元,"+taste+"味道的");
});
/*小龍發佈訂閱*/
Sesamecakeshop.addlisten("椒鹽",function(price,taste){
console.log("小龍發佈的"+price+"元,"+taste+"味道的");
});
Sesamecakeshop.trigger("椒鹽",10,"椒鹽");
Sesamecakeshop.remove("焦糖",fn1);//注意這裏是按照地址引用的。如果傳入匿名函數則刪除不了
Sesamecakeshop.trigger("焦糖",40,"焦糖");
推薦必讀:發佈-訂閱模式
11.扁平化後的數組
如:[1, [2, [ [3, 4], 5], 6]] => [1, 2, 3, 4, 5, 6]
var data = [1, [2, [ [3, 4], 5], 6]];
function flat(data, result) {
var i, d, len;
for (i = 0, len = data.length; i < len; ++i) {
d = data[i];
if (typeof d === 'number') {
result.push(d);
} else {
flat(d, result);
}
}
}
var result = [];
flat(data, result);
console.log(result);
12.冒泡排序
解析:
- 比較相鄰的兩個元素,如果前一個比後一個大,則交換位置。
- 第一輪的時候最後一個元素應該是最大的一個。
- 按照步驟一的方法進行相鄰兩個元素的比較,這個時候由於最後一個元素已經是最大的了,所以最後一個元素不用比較。
js代碼實現
function bubble_sort(arr){
for(var i = 0;i < arr.length - 1; i++){
for(var j = 0;j < arr.length - i - 1;j++){
if(arr[j] > arr[j+1]){
[arr[j], arr[j+1]] = [arr[j + 1], arr[j]]
}
}
}
}
var arr = [3,1,5,7,2,4,9,6,10,8];
bubble_sort(arr);
console.log(arr);
13.快速排序
快速排序是對冒泡排序的一種改進
解析:
- 第一趟排序時將數據分成兩部分,一部分比另一部分的所有數據都要小。
- 然後遞歸調用,在兩邊都實行快速排序。
js代碼實現
function quick_sort(arr){
if(arr.length <= 1){
return arr;
}
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0;i < arr.length; i++) {
if(arr[i] < pivot){
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quick_sort(left).concat([pivot],quick_sort(right));
}
var arr=[5,6,2,1,3,8,7,1,2,3,4,7];
console.log(quick_sort(arr));
14.選擇排序
// 選擇排序:大概思路是找到最小的放在第一位,找到第二小的放在第二位,以此類推 算法複雜度O(n^2)
選擇demo:
function selectionSort(arr) {
let len = arr.length;
let minIndex;
for (let i = 0; i < len - 1; i++) {
minIndex = i;
for (let j = i + 1; j < len; j++) {
if (arr[j] < arr[minIndex]) { //尋找最小的數
minIndex = j; //將最小數的索引保存
}
}
[arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
}
return arr;
}
本節參考文章:2018前端面試總結...
15.插入排序
解析:
- 從第一個元素開始,該元素可以認爲已經被排序
- 取出下一個元素,在已經排序的元素序列中從後向前掃描
- 如果該元素(已排序)大於新元素,將該元素移到下一位置
- 重複步驟3,直到找到已排序的元素小於或者等於新元素的位置
- 將新元素插入到下一位置中
- 重複步驟2
js代碼實現
function insert_sort(arr){
var i=1,
j,key,len=arr.length;
for(;i<len;i++){
var j=i;
var key=arr[j];
while(--j>-1){
if(arr[j]>key){
arr[j+1]=arr[j];
}else{
break;
}
}
arr[j+1]=key;
}
return arr;
}
或
function insert_sort(arr) {
let len = arr.length;
let preIndex, current;
for (let i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
insert_sort([2,34,54,2,5,1,7]);