🐲手写代码问题
# 🐲手写代码问题
# 手写Underscore
Underscore.js中文网 (underscorejs.net) (opens new window)
源码分析参考 underscore-analysis/underscore-1.8.3-analysis.js at master · lessfish/underscore-analysis (github.com) (opens new window) 前人栽树,后人乘凉~
# 集合函数 (数组 或对象) (opens new window)
# 数组函数(Array Functions) (opens new window)
# 与函数有关的函数(Function (uh, ahem) Functions) (opens new window)
# 对象函数(Object Functions) (opens new window)
# 实用功能(Utility Functions) (opens new window)
# JavaScript API
巩固基础知识,手写并深入理解JS-API
# 1.Array.reduce()
(opens new window)
# 语法
# 手写reduce
Array.prototype._reduce = function(callback, initialValue){
// 谁调用_reduce this就指向谁~ 为了语义化 这里把this赋值给arr
let arr = this;
if (typeof callback !== "function") {
throw "传入的回调函数捏?";
}
// 初始值有无——会导致 reduce归并操作中的累加器acc初始值 不同
let index = 0
if (!initialValue) {
index = 1;
acc = arr[0];
} else {
// 如果提供了初始值,则index从0开始~
acc = initialValue;
}
for(; index < arr.length; index++){
/**
* Accumulator (acc) (累计器) —— 必须传入
* Current Value (cur) (当前值) —— 必须传入
* Current Index (idx) (当前索引)
* Source Array (src) (源数组)
*/
acc = callback(acc, arr[index], index, arr)
}
return acc
}
let arr = [1,2,3]
console.log(arr._reduce((pre, cur) => pre + cur));// 6
console.log(arr._reduce((pre, cur) => pre + cur, 60));// 66
console.log(arr._reduce(666));// Uncaught 传入的回调函数捏?
网上搜到的版本中还写了方法用来判断arr是否为数组,那步没啥意义 它抛出错误会显示为—— arr._reduce is not a function
——毕竟是写在数组原型上的方法 不用数组调肯定是不行的 这个不用额外判断了~
# reduce的使用
求和
数组转为嵌套对象
['a', 'b', 'c'] -> {a: {b: {c: {}}}}
京东二面问题
reduce的最佳实践之一!也许会搭配手写reduce考察哦~
let arr = ['a', 'b', 'c'].reverse(); const transform = function transformArrToObj() { return arr.reduce((acc, cur) => { console.log(cur, acc) return { [cur]: acc } }, {}) } console.log("对象->JSON字符串 更好看", JSON.stringify(transform())); console.log("对象深拷贝", JSON.parse(JSON.stringify(transform())));
# 2.Object.create()
(opens new window)
# 语法
**Object.create()
**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。
const object1 = Object.create(Foo)
可用于实现单继承
——下面这种“寄生式继承”也是简单继承中的最优解 见这篇:(建议收藏)原生JS灵魂之问, 请问你能接得住几个? (opens new window) 这篇还对面向对象的设计思路进行了剖析
继承的最大问题在于:无法决定继承哪些属性,所有属性都得继承。——
代码的耦合性太高
,维护性不好。 ——面向组合的设计方式。来解决这个问题
// Shape - 父类(superclass)
function Shape() {
this.x = 0;
this.y = 0;
}
// 父类的方法
Shape.prototype.move = function(x, y) {
this.x += x;
this.y += y;
console.info('Shape moved.');
};
// Rectangle - 子类(subclass)
function Rectangle() {
// 继承属性
Shape.call(this); // call super constructor.
}
// 子类续承父类
Rectangle.prototype = Object.create(Shape.prototype); // Rectangle.prototype = new Shape(); 也是可以的
Rectangle.prototype.constructor = Rectangle;
var rect = new Rectangle();
console.log('Is rect an instance of Rectangle?',
rect instanceof Rectangle); // true
console.log('Is rect an instance of Shape?',
rect instanceof Shape); // true —— 继承成功
rect.move(1, 1); // Outputs, 'Shape moved.'
(如果你希望能继承到多个对象,则可以使用混入的方式。)
// 继承两个父类的属性 function MyClass() { SuperClass.call(this); OtherSuperClass.call(this); } // 继承一个类 MyClass.prototype = Object.create(SuperClass.prototype); // 混合其它 Object.assign(MyClass.prototype, OtherSuperClass.prototype); // 重新指定constructor MyClass.prototype.constructor = MyClass; MyClass.prototype.myMethod = function() { // do a thing };
Object.assign (opens new window) 会把
OtherSuperClass
原型上的函数拷贝到MyClass
原型上,使 MyClass 的所有实例都可用 OtherSuperClass 的方法。Object.assign 是在 ES2015 引入的,且可用 polyfilled (opens new window)。
# 手写create
思路:将传入的父类原型对象作为原型,返回继承了父类(方法)的子类的实例对象。
const create = function(objPrototype) {
function Son() {};
Son.prototype = objPrototype;
return new Son();
}
验证一下:
function Dad() {
this.attr = 0;
}
// 父类的方法
Dad.prototype.change = function(x, y) {
this.attr += x;
console.info(this.attr, 'attr changed.');
};
function Son() {
// 继承属性
Dad.call(this); // call super constructor.
}
/**
* 第一行利用create方法达成子类续承父类的步骤👇
* constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的。
* 第二行为了纠正子类的构造函数指向正确
* create方法中传入new Dad()实例对象也可以完成继承,但是为了更符合create方法,我们传入Dad.prototype
*/
Son.prototype = create(Dad.prototype);
Son.prototype.constructor = Son;
var son = new Son();
// 验证继承是否成功——父类的 属性 & 方法
console.log('Is son an instance of Son?',
son instanceof Son); // true
console.log('Is son an instance of Dad?',
son instanceof Dad); // true —— 继承成功
son.change(666); // 666, 'attr changed.'
# 3.instanceof
(opens new window)
# 语法
f1 instanceof Foo
instanceof
运算符用于检测构造函数(Foo)的 prototype
属性是否出现在某个实例对象(f1)的原型链上。
function Car(make, model, year) {
this.make = make;
this.model = model;
this.year = year;
}
const auto = new Car('Honda', 'Accord', 1998);
console.log(auto instanceof Car);
// expected output: true
console.log(auto instanceof Object);
// expected output: true
console.log(auto instanceof Function);
// expected output: false
由下图可知 f1的原型链上有f1的构造函数Foo、Object
f1.__proto__ = Foo.prototype;
f1.__proto__ = Object.prototype;
f1的构造函数Foo的原型链上有 Function、Object
# 手写instanceof
f1 instanceof Foo
1.获取类型的原型
f1.__proto__
2.获取构造函数的
prototype对象
(显式原型对象)3.顺着原型链往下找,循环判断
function myInstanceof(f1, Foo) {
let proto = Object.getPrototypeOf(f1), // 获取对象的原型
prototype = Foo.prototype; // 获取构造函数的 prototype 对象
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (proto !== null) {
if (proto === prototype) return true;
// 返回对象(proto)的原型
proto = Object.getPrototypeOf(proto);
}
// 走到原型链尽头
return false;
}
# Object.getPrototypeOf() (opens new window)
Object.getPrototypeOf()
方法返回指定对象的(显式)原型(内部[[Prototype]]
属性的值, 也就是__proto__
)。
补充一个更完善的例子
# 验证
var Foo = function() {}
const f1 = new Foo();
console.log(myInstanceof(f1, Foo)); // true
console.log(myInstanceof(f1, Object)); // true
console.log(myInstanceof(f1, Function)); // false
# 4.链式调用
函数内容皆为 return this
# 5.Promise
# 数据处理问题
# 1.实现日期格式化函数
# 要求:
dateFormat(new Date('2020-12-01'), 'yyyy/MM/dd'); // 2020/12/01
dateFormat(new Date('2020-04-01'), 'yyyy年MM月dd日') // 2020年04月01日
# 实现
思路:
String.prototype.replace
Date类型的对象相关API
.getDate()/getMonth()/getFullYear()
const dateFormat = function(dateInput, format) {
let year = dateInput.getFullYear();
let month = dateInput.getMonth() + 1;
let day = dateInput.getDate();
// 可以使用正则表达式作为replace方法的“模式”
format = format.replace(/yyyy/, year);
// 可以使用字符串作为replace方法的“模式“
format = format.replace('MM', month);
format = format.replace('dd', day);
return format;
}
可以用上面“要求”中的内容进行测试
public timeFormat(rowDate: Date (opens new window) | String (opens new window) | number (opens new window), fmt: String (opens new window)): String (opens new window)source (opens new window)
format type by fmt
# Params:
Name | Type | Attribute | Description |
---|---|---|---|
rowDate | Date (opens new window) | String (opens new window) | number (opens new window) | [date instance|date string|timestamp] | |
fmt | String (opens new window) | eg:YYYY-MM-DD HH:mm:ss |
# Return:
String (opens new window) formated date
# Example:
timeformat('2018-10-10 10:30:20', 'MM/DD HH:mm')
// => 10/10 10:30
const date = new Date('2018-10-10 10:30:20')
timeformat(date, 'MM/DD HH:mm')
// => 10/10 10:30
const date = new Date('2018-10-10 10:30:20')
timeformat(date.getTime(), 'MM/DD HH:mm')
// => 10/10 10:30
# answer
Date.prototype.toISOString() (opens new window)
toISOString()
方法返回一个 ISO 格式的字符串: YYYY-MM-DDTHH:mm:ss.sssZ。时区总是UTC(协调世界时),加一个后缀“Z”标识。
/**
* format type by fmt
* @param {Date|String|number} rowDate [date instance|date string|timestamp]
* @param {String} fmt eg:YYYY-MM-DD HH:mm:ss
* @return {String} formated date
*
* @example
* timeformat('2018-10-10 10:30:20', 'MM/DD HH:mm')
* // => 10/10 10:30
*
* const date = new Date('2018-10-10 10:30:20')
* timeformat(date, 'MM/DD HH:mm')
* // => 10/10 10:30
*
* const date = new Date('2018-10-10 10:30:20')
* timeformat(date.getTime(), 'MM/DD HH:mm')
* // => 10/10 10:30
*
*/
const timeformat = (rowDate, fmt) => {
if (!rowDate || !fmt) return fmt;
// get correct date
const date = new Date(rowDate);
// console.log(date);
if (date.toString() === 'Invalid Date') {
return fmt;
}
// 输入的时间是中国时区的 UTC时间比中国时间提前八个小时 所以要打出来8小时的富余量~
// 使用setHours方法
date.setHours(date.getHours() + 8);
// console.log(date);
const [YYYY, MM, DD, HH, mm, ss] = date.toISOString().match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})/);
// console.log(date.toISOString());
return fmt.replace('YYYY', YYYY)
.replace('YY', YYYY.slice(-2))
.replace('MM', MM)
.replace('DD', DD)
.replace('HH', HH)
.replace('mm', mm)
.replace('ss', ss);
};
# 2. 交换a,b的值-异或
# 要求
不能使用临时变量
# 实现
巧妙加减
let a = 1, b = 2;
a = a + b; // 1 + 2 = 3
b = a - b; // 3 - 2 = 1
a = a - b; // 3 - 1 = 2
利用异或运算
- a ^ a = 0
- 0 ^ a = a
let a = 1, b = 2;
a = a ^ b;
b = a ^ b; // (a ^ b) ^ b = a
a = a ^ b; // (a ^ b) ^ a = b
# 3.数组转为嵌套对象 ['a', 'b', 'c'] -> {a: {b: {c: {}}}}
京东二面问题
reduce的最佳实践!也许会搭配手写reduce考察哦~
let arr = ['a', 'b', 'c'].reverse();
const transform = function transformArrToObj() {
return arr.reduce((acc, cur) => {
console.log(cur, acc)
return { [cur]: acc }
}, {})
}
console.log("对象->JSON字符串 更好看", JSON.stringify(transform()));
console.log("对象深拷贝", JSON.parse(JSON.stringify(transform())));
# 4.数组扁平化
flat
let tempArr = [1,2,3,[4,5],6];
tempArr.flat(Infinity); // 无限层的扁平化
- 转字符串
let tempArr = [1,2,3,[4,5],6];
let result = tempArr.join(',').split(',').map((str) => Number(str));
// 或者使用toString()
let result = tempArr.toString().split(",").map((str) => Number(str));
reduce
- 从
arr
第一项开始(所以要给acc累加器加上初始值{}
) - 如果遇到的值为数组 则扔到递归调用栈里进行递归遍历 , 否则直接接到acc的尾部(使用
concat
)
- 从
const flatten = (arr) => {
return arr.reduce((acc, cur) => {
// 根据“cur是否为数组”进行递归~
return acc.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
扩展运算符
[].concat(...arr)
// 多维数组 const flatten = (arr) => { while(arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; }
- 上面的reduce也可以和扩展运算符结合~
const flatten = (arr) => { return arr.reduce((acc, cur) => { return Array.isArray(cur) ? [...acc, ...cur] : [...acc, cur]; }, []) }
# 5.超级全的数组去重
# 0.超级无敌万能方法
22/3/18美团一面遇到的 数组中元素比较复杂?没关系~
使用obj存储 数组中的每个元素 结合filter 如果碰到了重复的元素就删去
let arr=[{}, {}, NaN, NaN, {a: 1}, {a: 1}, {a: 1, b: 666}, {a: 1, b: 666}, [], [], 666, '666', '666', 666, false, false];
Array.prototype.uniq = function() {
const uniqObj = {};
return this.filter((item) => {
let uniqItem = typeof item + JSON.stringify(item);
return uniqObj.hasOwnProperty(uniqItem) ? false : uniqObj[uniqItem] = true; // obj[attr] = true 返回true obj[attr] = false 返回false 22/3/20的sfz并不知道这个知识点,,,
})
}
console.log(arr.uniq()); // 测试成功~ 爽~~
# 1. 数组元素比较型
双层
for
循环// 双层for循环 // 前一个跟之后所有进行比较 , 重复了删除掉 function uniq(arr){ for(let i = 0 ; i < arr.length - 1 ; i++){ for(let j = i + 1 ; j < arr.length; j++){ if(arr[i] === arr[j]){ arr.splice(i , 1); // 删除后下表移动到原位置 j--; } } } return arr; }
排序后 相邻位置进行比较
// 排序进行后进行相邻比较 function sortQ(arr){ // 排序后 // 没参数 如果没有指明 compareFunction ,那么元素会按照转换为的字符串的诸个字符的Unicode位点进行排序。 arr.sort(); for(let i = 0 ; i < arr.length - 1 ; i++){ if(arr[i] === arr[i + 1]){ arr.splice(i , 1); i--; } } }
# 2.查找元素位置型
indexOf
查找元素并返回其第一个索引值function uniq(arr){ const res = []; for(let i = 0 ; i < arr.length ; i++){ if(arr.indexOf(arr[i]) === i){ res.push(arr[i]) } } return res; }
findIndex
返回数组中第一个满足测试函数的元素的索引function uniq(arr){ const res = []; for(let i = 0 ; i < arr.length ; i++){ if(arr.findIndex((item) => item === arr[i]) === i ){ res.push(arr[i]); } } return res; }
# 3. 查找元素存在型
includes
// 查找元素存在型 function uniq(arr){ const res = []; for(let i = 0 ; i < arr.length ; i++){ if(!res.includes(arr[i])){ res.push(arr[i]) } } return res; }
# 4. 利用数据结构类型
set
// set function uniq(arr){ return [...new Set(arr)] }
map
function uniq(arr){ const map = new Map(); arr.forEach(item =>{ map.set(item, true) }) // 返回键值 Object.keys(key) return[...map.keys()] }
# 5. 总结
在简单的测试用例大概 2000 万条数据下,indexOf
的方案速度相对最快
# 6.实现数组的乱序输出
# 前端面试(算法篇) - 数组乱序 (opens new window)
写得挺好的 搭配 384. 打乱数组 (opens new window) 食用更佳
首先是伪随机的两种方法
- 【1】随机取数
从原数组中随机抽取一个数,然后使用 splice 删掉该元素
function getRandomArrElement(arr, count) {
let res = []
while (res.length < count) {
// 生成随机 index
let randomIdx = (Math.random() * arr.length) >> 0;
// splice 返回的是一个数组
res.push(arr.splice(randomIdx, 1)[0]);
}
return res
}
上面生成随机 index 用到了按位右移操作符 >>
当后面的操作数是 0 的时候,该语句的结果就和 Math.floor() 一样,是向下取整
但位操作符是在数值表示的最底层执行操作,因此速度更快
// 按位右移
(Math.random() * 100) >> 0
// Math.floor
Math.floor(Math.random() * 100)
/* 这两种写法的结果是一样的,但位操作的效率更高 */
- 【2】通过 sort 乱序
首先认识一下 Array.prototype.sort()
(opens new window) 不了解的查看下 这个必须滚瓜烂熟
let arr = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
arr.sort((a, b) => 0.5 - Math.random());
但这并不是真正的乱序,计算机的 random 函数因为循环周期的存在,无法生成真正的随机数(力扣那题用这个方法也跑不通XD)
- 【3】Fisher–Yates shuffle 洗牌算法
这个方法太漂亮勒!
另外这个作者的写法也很漂亮!棒!
洗牌算法的思路是:
1.先从数组末尾开始,选取最后一个元素,与数组中随机一个位置的元素交换位置
2.然后在已经排好的最后一个元素以外的位置中,随机产生一个位置,让该位置元素与倒数第二个元素进行交换
以此类推,打乱整个数组的顺序
function shuffle(arr) {
let len = arr.length;
while (len) {
let i = (Math.random() * len--) >> 0;// 获得随机数
// 交换位置
let temp = arr[len];
arr[len] = arr[i];
arr[i] = temp;
}
return arr;
}
再结合 ES6 的解构赋值,使用洗牌算法就更方便了:
Array.prototype.shuffle = function() {
let m = this.length, i;
while (m) {
i = (Math.random() * m--) >> 0;
[this[m], this[i]] = [this[i], this[m]]
}
return this;
}
# 7.排序问题
# 场景应用问题
# 1.循环打印红黄绿-经典异步编程问题
# 2.sleep函数
字节二面曾遇到过的问题,在组内工具函数库中看到了,试着写一写
- Promise.resolve() (opens new window)方法返回一个以给定值解析后的
Promise
(opens new window) 对象。- 如果这个值是一个 promise ,那么将返回这个 promise
- 如果这个值是thenable(即带有
"then"
(opens new window)方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态 - 否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
/**
* 睡眠指定时间 返回promise实例
* @param {int} duration 睡眠时间, 毫秒
* @return {Promise Instance} Promise 实例
*
* @example
* sleep(100)
* => Promise {<pending>}
*
*/
const sleep = (duration) => new Promise((resolve) => {
// 写法1
setTimeout(() => {
resolve();
}, duration)
// 写法2
// setTimeout(resolve, time)
});
sleep(1000).then(() => {console.log("sleep")});
// 也可以封装得再彻底一些,像这样👇(不过这样灵活性不好,上述方式更加合理优秀!)
const sleep = (duration) => new Promise((resolve) => {
setTimeout(() => {
resolve();
}, duration)
}).then(() => {
console.log("sleep");
});
sleep(1000);
# 4.debounce
防抖函数
# 定义
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。
这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
# 用途
- 当用户在短时间内,多次点击登陆,发送短信等请求数据的操作时
- 文本编辑器一段时间不操作,进行自动保存
- 搜索框的联想——实时发送请求
- 用户输入文字,只在停顿1s时才进行联想
# 手写防抖函数
//模拟一段ajax请求
function ajax(content) {
console.log('ajax request ' + content)
}
function debounce(fun, delay) {
// 绑定函数上下文d
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
con
fun.apply(this, args);
}, delay)
}
}
let inputb = document.getElementById('debounce')
let debounceAjax = debounce(ajax, 500)
inputb.addEventListener('keyup', function (e) {
debounceAjax(e.target.value)
})
# 具体应用
# 尤大的防抖
# 5.throttle
节流函数
# 定义
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。
通过事件节流来降低事件调用的频率。
# 用途
- scroll 函数(页面滚动)的事件监听上,通过事件节流来降低事件调用的频率(每隔1s(固定的时间)才计算一次位置信息,避免边滚动边计算)。
- 鼠标mouseover事件,只执行一次时
# 手写节流函数
function throttle(fun, delay) {
let deferTimer;
let preTime = Date.now();
return function (args) {
let that = this
let _args = arguments
let now = +new Date()
console.log(now);
if (preTime && now < preTime + delay) {
// 还没到节流规定的单位时间
clearTimeout(deferTimer)
deferTimer = setTimeout(function () {
preTime = now
fun.apply(that, _args)
}, delay)
} else {
// 到了节流规定的单位时间
preTime = now
fun.apply(that, _args)
}
}
}
# 6.对象深拷贝
# 简单的对象深拷贝
判断当前键对应的值是否为Object类型 + 递归
- 缺陷:无法解决循环引用问题 比如:target.target = target
const deepClone = (target) => {
if (typeof target === 'object') {
const cloneTarget = Array.isArray(target) ? [] : {};
for (const attr in target) {
cloneTarget[attr] = deepClone(target[attr]);
}
return cloneTarget;
} else {
return target;
}
};
# 测试
const testTarget = {
a: 666,
b: [1, 2, 3],
c: { d: '666' },
};
// 基础版本的deepClone无法解决循环引用问题,会爆栈~
// testTarget.testTarget = testTarget;
const copy = deepClone(testTarget);
copy.b = ['修改深拷贝过来的引用数据类型', '也没有关系~'];
console.log('deepClone:', copy); // deepClone: { a: 666, b: [ '修改深拷贝过来的引用数据类型', '也没有关系~' ], c: { d: '666' } }
console.log('origin:', testTarget); // origin: { a: 666, b: [ 1, 2, 3 ], c: { d: '666' } }
# 7.将拍平对象转换为层级对象
生成新的对象(默认不会出现嵌套情况)
相同父集需要做归并处理
// strarr -> obj
let strarr = {
'a.b.c.d': 1,
'a.b.c.e': 2,
'a.b.f': 3,
'a.j': 4,
};
let obj = {
a: {
b: {
c: {
d: 1,
e: 2,
},
f: 3,
},
j: 4,
},
};
- 方法1
function fn(arr) {
const values = Object.keys(arr);
let obj = {};
for (let i = 0; i < values.length; i++) {
let value = values[i];
let keyArr = value.split('.');
for (let j = 0; j < keyArr.length; j++) {
if (j == 0) {
var pre = !obj[keyArr[j]]
? (obj[keyArr[j]] = {})
: obj[keyArr[j]];
} else if (j != keyArr.length - 1) {
pre = !pre[keyArr[j]] ? (pre[keyArr[j]] = {}) : pre[keyArr[j]];
} else {
pre[keyArr[j]] = arr[value];
}
}
pre = {};
}
return obj;
}