JS 手写题目汇总
总结面试中常见的 JavaScript 手写题,比如,call、apply、bind、promise 等等,只有能够游刃有余的熟练掌握此类面试常见的 JavaScript 手写题目,才能在面试中让面试官眼前一亮,话不多说,直接上题目。
1、实现 call、apply、bind
1、实现 call
作用:使用一个指定的 this
值和一个或多个参数来调用一个函数;
- 第一个参数为
null
或者undefined
时,this
指向全局对象window
,值为原始值的指向该原始值的自动包装对象,如String、Number、Boolean
; - 为了避免函数名与上下文(
context
)的属性发生冲突,使用Symbol
类型作为唯一值; - 将函数作为传入的上下文(
context
)属性执行; - 函数执行完成后删除该属性;
- 返回执行结果;
// ES5
Function.prototype.call2 = function(context) {
if (typeof this !== 'function') {
throw new TypeError('Type Error');
}
var context = context || window;
// 将 this 绑定到对象上的一个方法
context.fn = this;
var args = [];
// 储存函数变量参数
for (var i=1; i<arguments.length; i++) {
args.push('arguments[' + i + ']');
}
// 执行 context 上刚刚绑定的方法
var result = eval('context.fn(' + args + ')');
// 删除刚刚创建的方法
delete context.fn;
// 返回函数调用的返回值
return result;
}
// ES6 (推荐)
// 传递参数从一个数组变成逐个传参了,不用...扩展运算符的也可以用 arguments 代替
Function.prototype.call2 = function (context, ...args) {
// 这里默认不传就是给 window, 也可以用 ES6 给参数设置默认参数
context = context || window;
args = args ? args : [];
// 给 context 新增一个独一无二的属性以免覆盖原有属性
const key = Symbol();
context[key] = this;
// 通过隐式绑定的方式调用函数
const result = context[key](...args);
// 删除添加的属性
delete context[key];
// 返回函数调用的返回值
return result;
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
console.log(bar.call2(null)); // 2
console.log(bar.call2(obj, 'kevin', 18)); // 1
复制代码
2、实现 apply
作用:跟 call
一样,唯一的区别就是 call
是传入不固定个数的参数,而 apply
是传入一个数组;
- 前部分与
call
一样; - 第二个参数可以不传,但类型必须为数组或者类数组;
// ES5
Function.prototype.apply2 = function(context,arr) {
if (typeof this !== 'function') {
throw new TypeError('Type Error');
}
var context = context || window;
context.fn = this;
if (!arr) {
var result = context.fn();
} else {
var args = [];
for (var i=0; i<arr.length; i++) {
args.push('arr[' + i + ']');
}
var result = eval('context.fn(' + args + ')');
}
delete context.fn;
return result;
}
// ES6 (推荐)
Function.prototype.apply2 = function (context, args) {
// 这里默认不传就是给 window, 也可以用 ES6 给参数设置默认参数
context = context || window;
args = args ? args : [];
// 给 context 新增一个独一无二的属性以免覆盖原有属性
const key = Symbol();
context[key] = this;
// 通过隐式绑定的方式调用函数
const result = context[key](...args);
// 删除添加的属性
delete context[key];
// 返回函数调用的返回值
return result;
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
console.log(bar.apply2(null)); // 2
console.log(bar.apply2(obj, 'kevin', 18)); // 1
复制代码
3、实现 bind
作用:bind
方法会创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用;
需要考虑:
bind()
除了this
外,还可传入多个参数;bind
创建的新函数可能传入多个参数;- 新函数可能被当做构造函数调用;
- 函数可能有返回值;
实现方法:
bind
方法不会立即执行,需要返回一个待执行的函数;(闭包)- 实现作用域绑定(
apply
); - 参数传递(
apply
的数组传参); - 当作为
构造函数
的时候,进行原型继承
;
// ES5
Function.prototype.bind = function (oThis) {
var aArgs = Array.prototype.slice.call(arguments, 1);
var fToBind = this;
var fNOP = function () {};
var fBound = function () {
fBound.prototype = this instanceof fNOP ? new fNOP() : fBound.prototype;
return fToBind.apply(this instanceof fNOP ? this : oThis || this, aArgs )
}
if( this.prototype ) {
fNOP.prototype = this.prototype;
}
return fBound;
}
// ES6 (推荐)
Function.prototype.bind2 = function (context, ...args) {
const fn = this;
args = args ? args : [];
return function newFn(...newFnArgs) {
// 判断是否作为构造函数使用的情况
if (this instanceof newFn) {
return new fn(...args, ...newFnArgs)
}
// 当作为普通函数执行,用 apply 执行函数
return fn.apply(context, [...args,...newFnArgs])
}
}
// 测试一下
var value = 2;
var obj = {
value: 1
}
function bar(name, age) {
console.log(this.value);
return {
value: this.value,
name: name,
age: age
}
}
const b = bar.bind2(null, 'kevin'); // 2
b(18) // 2
const b_ = bar.bind2(obj, 'kevin');
b_(18); // 1
复制代码
2、实现 new
作用:new
运算符用来创建用户自定义的对象类型的实例或者具有构造函数的内置对象的实例。
- 创建一个新对象;
- 设置原型,将对象的原型设置为函数的
prototype
对象; - 让函数的
this
指向这个对象,并执行构造函数的代码 (为这个新对象添加属性); - 判断函数的返回值类型,如果是值类型,返回创建的对象, 如果是引用类型,就返回这个引用类型的对象;
// ES5 (推荐)
function newFunc() {
// 创建一个新对象
var obj = new Object();
// 将第一个参数提取出来,也就是构造函数
var Constructor = [].shift.call(arguments);
// 设置原型,将对象的原型设置为函数的 `prototype` 对象。
obj.__proto__ = Constructor.prototype;
// 让函数的 `this` 指向这个对象,并执行构造函数的代码(为这个新对象添加属性)
var ret = Constructor.apply(obj, arguments);
// 判断函数的返回值类型,如果是值类型,返回新创建的对象。如果是引用类型,就返回这个引用类型的对象。
return typeof ret === 'object' ? ret : obj;
}
// ES6 (推荐)
function newFunc(...args) {
const obj = Object.create({});
const Constructor = [].shift.call(args);
obj.__proto__ = Constructor.prototype;
const result = Constructor.apply(obj, args);
return typeof result === 'object' ? result : obj;
}
function F(name, age) {
this.name = name;
this.age = age
}
const newF = newFunc(F, 'dell', 18);
console.log(newF.name, newF.age); // dell 18
复制代码
3、7 种继承方式
1、原型链继承
优点:
- 能够继承父类的属性和方法;
缺点:
- 问题1:原型中包含的
引用类型属性
将被所有实例共享(不能实现多继承
); - 问题2:
子类实例化
的时候不能给父类构造函数传参; - 问题3:子类实例化后不能获取子类原型上的属性和方法;
function Parent() {
this.name = 'parent1';
this.play = [1, 2, 3];
this.add = function() {
return {
name: this.name,
play: [1,2,3]
}
}
}
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
this.type = 'child2';
this.add2 = function() {
return this.type;
}
}
Child.prototype.add1 = function() {
return this.type;
}
Child.prototype.dell = 'dell';
// 原型链继承
Child.prototype = new Parent();
// 创建实例
var c = new Child();
console.log(c.name, c.type, c.add(), c.add2(), c.getName()); // 都可以获取到
// 无法读取本身原型上的属性和方法
console.log(c.add1()); // 不能读取到
console.log(c.dell);
// 不能使用多继承,可以修改父级原型上的属性,但如果这个属性是引用属性,那就会共享同一个实例,如下:
var child1 = new Child();
var child2 = new Child();
child1.name = 'dell';
child1.play[2] = 56;
console.log(child1.name); // dell
console.log(child2.name); // parent1
console.log(child1.play); // [1,2,56]
console.log(child2.play); // [1,2,56]
复制代码
2、构造函数继承
优点:
- 解决了原型链继承的问题 (1、不能实现多继承;2、子类实例化时不能向构造函数传参);
缺点:
- 只能继承父类的属性和方法,不能继承父类原型上的属性和方法;
- 由于方法是定义在构造函数中,所以每次子类实例化时都会创建一次方法;
function Parent(name) {
this.name = name;
this.age = '18';
this.play = [1, 2, 3];
this.getAge = function() {
return this.age;
}
}
Parent.prototype.lee = 'lee';
Parent.prototype.getName = function () {
return this.name;
}
function Child(name) {
// 传参
Parent.call(this, name); // 继承父类
this.type = 'child2';
this.getDell_ = function() {
return this.age;
}
}
Child.prototype.getDell = function() {
return this.age;
}
// 创建实例
var c1 = new Child('name');
var c2 = new Child('name');
// 可以获取子类原型属性和方法
console.log(c1.name, c1.age, c1.play, c1.getAge(), c1.getDell_(), c1.getDell()) // parent1 18 18 18 18
// 可以实现多继承 即不再共享同一个实例
c1.play[1] = 99
console.log(c1.play) // [1, 99, 3]
console.log(c2.play) // [1, 2, 3]
// 不能获取父类原型上的属性和方法
console.log(c1.lee) // undefined
console.log(c1.getName()) // 报错
复制代码
3、组合继承
优点:
- 解决了原型链继承或者构造函数继承单独使用产生的问题;
缺点:
- 由于组合继承是结合了原型链继承和构造函数继承的方式,在原型链继承和构造函数继承都调用了一次父类,因此会使得父类被调用了 2 次;
function Parent() {
console.log('两次调用')
this.name = 'parent1';
this.age = '18';
this.play = [1, 2, 3];
this.getAge = function() {
return this.age
}
}
Parent.prototype.lee = 'lee'
Parent.prototype.getName = function () {
return this.name;
}
function Child() {
Parent.call(this); // 第二次调用 Parent
this.type = 'child2';
}
Child.prototype.getDell = function() {
return this.age
}
Child.prototype = new Parent(); // 第一次调用 Parent
Child.prototype.constructor = Child; // 手动挂上 constructor 避免不能访问到 Parent
var c = new Child();
console.log(c.name, c.age, c.getAge()) // parent1 18 18
console.log(c.lee) // lee
console.log(c.getName()) // parent1
复制代码
4. 原型式继承
优点:
- 能够继承父类的属性和方法;
缺点:
- 没有解决引用类型共享问题(不能实现多继承);
var Parent = {
name: 'dell',
arr: [18, 19, 20],
getName: function() {
return this.name
}
}
let p1 = Object.create(Parent);
p1.name = 'tom'
p1.arr.push('lee')
let p2 = Object.create(Parent);
p2.name = 'lee';
p2.arr.push('dell')
console.log(p1.name, p1.arr, p1.getName())// tom [18, 19, 20, "lee", "dell"] tom
console.log(p2.name, p2.arr, p2.getName()) // lee [18, 19, 20, "lee", "dell"] lee
// 优点:能够继承属性和方法 缺点:没有解决引用问题,创建出来的引用时一样的
复制代码
5、寄生式继承
优点:
- 能够继承父类的属性和方法;
缺点:
- 没有解决引用类型共享问题(不能实现多继承);
var Parent = {
name: 'dell',
arr: [18, 19, 20],
getName: function() {
return this.name
}
}
let p1 = Object.create(Parent);
p1.getArr = function() {
return this.name
}
console.log(p1.name, p1.arr, p1.getName(), p1.getArr()) // dell [18, 19, 20] dell dell
复制代码
6、组合寄生式继承
优点:
- 解决了组合继承方式出现的调用 2 次父类的问题(通过 Object.create(Parent.prototype) 创建一个副本,再副本上进行继承就不会出现调用两次父类的问题)
缺点:
- 书写方式过于麻烦,推荐使用 ES6 class 继承方式;
function Parent() {
console.log('执行一次,没有执行两次')
this.name = 'dell'
this.age = 18
this.getName = function() {
return this.name
}
}
Parent.prototype.getAge = function() {
return this.age;
}
function Child() {
Parent.call(this);
this.type = 'type'
}
Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;
// 创建实例
var p = new Child();
console.log(p.name, p.age, p.getName()) // dell 18 dell
console.log(p.getAge()) // 18
console.log(p.type) // type
// console.log(c.getDell()) // 为啥访问不到子类原型上的方法呢
复制代码
7、ES6 继承方式
// class 相当于 ES5 中构造函数;
// class 中定义方法时,前后不能加 function,全部定义在 class 的 protopyte 属性中;
// class 中定义的所有方法是不可枚举的;
// class 中只能定义方法,不能定义对象、变量等;
// class 和方法内默认都是严格模式;
// ES5 中 constructor 为隐式属性;
class People {
constructor(name='dai',age='27') {
this.name = name;
this.age = age;
}
eat() {
console.log(`${this.name} ${this.age} eat food`);
}
}
// 继承父类
class Woman extends People {
constructor(name = 'dai',age = '27') {
// 继承父类属性
super(name, age);
}
eat() {
// 继承父类方法
super.eat()
}
}
let wonmanObj = new Woman('dai');
wonmanObj.eat();
// ES5 继承先创建子类的实例对象,然后再将父类的方法添加到 this 上(Parent.apply(this));
// ES6 继承是使用关键字 super 先创建父类的实例对象 this,最后在子类 class 中修改 this;
复制代码
4、节流与防抖
1. 防抖 (debounce)
- 高频事件触发后,n 秒后只执行一次,如果 n 秒内再次被触发,则重新计算时间;
- 场景:input 搜索框搜索输入;
<input oninput="debounceInput(event)" />
function debounce(fn, delay) {
var timeout = null;
return function() {
// 清除定时器,重新计算时间
clearTimeout(timeout);
// 延时,时间到后执行函数
timeout = setTimeout(() => {
fn.apply(this, arguments)
}, delay)
}
}
function onInput(event) {
if (event.target.value) {
console.log(event.target.value);
}
}
let debounceInput = debounce(onInput, 300)
// 用途: 用于 input 框的搜索
复制代码
2. 节流
- 高频事件触发后,n 秒内只执行一次,节流是稀释函数的执行频率;
- 场景:长列表滚动节流、resize;
2.1 时间戳
- 首节流,第一次立刻执行,但是停止出发后,没办法再执行;
// 时间戳实现方式
function throttle(fn, delay) {
var last = 0;
return function () {
var now = Date.now();
if (now - last >= delay) {
last = now;
fn.apply(this, arguments)
}
}
}
复制代码
2.2 setTimeout
- 尾节流,不会立刻执行函数,而是在 delay 之后执行函数;
// 定时器实现
// 最后一次停止触发后,因为 delay 的定时器,还会最后执行一次;
function throttle(fn, delay) {
var timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay)
}
}
}
复制代码
2.3 时间戳 + setTimeout
- 实现了首节流和尾节流的方式
// 时间戳+定时器实现
function throttle(fn, delay){
let timer = null;
let startTime = 0;
return function() {
let curTime = Date.now();
let remaining = delay - (curTime - startTime);
clearTimeout(timer);
if (remaining <= 0) {
fn.aplly(this, arguments);
startTime = Date.now();
} else {
timer = setTimeout(() => {
fn.aplly(this, arguments);
startTime = Date.now();
}, remaining)
}
}
}
复制代码
5、Promise 系列手写
1. 手写 Promise 基础版
class Prom {
static resolve (value) {
if (value && value.then) {
return value
}
return new Prom(resolve => resolve(value))
}
constructor (fn) {
this.value = undefined
this.reason = undefined
this.status = 'PENDING'
// 维护一个 resolve/pending 的函数队列
this.resolveFns = []
this.rejectFns = []
const resolve = (value) => {
// 注意此处的 setTimeout
setTimeout(() => {
this.status = 'RESOLVED'
this.value = value
this.resolveFns.forEach(({ fn, resolve: res, reject: rej }) => res(fn(value)))
})
}
const reject = (e) => {
setTimeout(() => {
this.status = 'REJECTED'
this.reason = e
this.rejectFns.forEach(({ fn, resolve: res, reject: rej }) => rej(fn(e)))
})
}
fn(resolve, reject)
}
then (fn) {
if (this.status === 'RESOLVED') {
const result = fn(this.value)
// 需要返回一个 Promise
// 如果状态为 resolved,直接执行
return Prom.resolve(result)
}
if (this.status === 'PENDING') {
// 也是返回一个 Promise
return new Prom((resolve, reject) => {
// 推进队列中,resolved 后统一执行
this.resolveFns.push({ fn, resolve, reject })
})
}
}
catch (fn) {
if (this.status === 'REJECTED') {
const result = fn(this.value)
return Prom.resolve(result)
}
if (this.status === 'PENDING') {
return new Prom((resolve, reject) => {
this.rejectFns.push({ fn, resolve, reject })
})
}
}
}
Prom.resolve(10).then(o => o * 10).then(o => o + 10).then(o => {
console.log(o)
})
return new Prom((resolve, reject) => {
reject('Error')
}).catch(e => {
console.log('Error', e)
})
复制代码
2. Promise.resolve
Promise.resolve(value)
可以将任何值转成值为value
状态是fulfilled
的Promise
,但如果传入的值本身是Promise
则会原样返回它;
Promise.resolve = (param) => {
if(param instanceof Promise) return param;
return new Promise((resolve, reject) => {
if(param && param.then && typeof param.then === 'function') {
// param 状态变为成功会调用 resolve,将新 Promise 的状态变为成功,反之亦然
param.then(resolve, reject);
} else {
resolve(param);
}
})
}
复制代码
3. Promise.reject
- 和
Promise.resolve()
类似,Promise.reject()
会实例化一个rejected
状态的Promise
。但与Promise.resolve()
不同的是,如果给Promise.reject()
传递一个Promise
对象,则这个对象会成为新Promise
的值;
Promise.reject = function(reason) {
return new Promise((resolve, reject) => reject(reason))
}
复制代码
4. Promise.prototype.finally
- 不管成功还是失败,都会走到
finally
中,并且finally
之后,还可以继续then
。并且会将值原封不动的传递给后面的then
;
Promise.prototype.finally = function (callback) {
return this.then((value) => {
return Promise.resolve(callback()).then(() => {
return value;
});
}, (err) => {
return Promise.resolve(callback()).then(() => {
throw err;
});
});
}
复制代码
5. Promise.all
- 传入的所有
Promsie
都是fulfilled
,则返回由他们的值组成的,状态为fulfilled
的新Promise
; - 只要有一个
Promise
是rejected
,则返回rejected
状态的新Promsie
,且它的值是第一个rejected
的Promise
的值; - 只要有一个
Promise
是pending
,则返回一个pending
状态的新Promise
;
function PromiseAll(promiseArray) {
return new Promise((resolve, reject) => {
if (!Array.isArray(promiseArray)) {
return reject(new Error('传入的参数必须是数组!'));
}
let arr = [];
const len = promiseArray.length;
let counter = 0;
for (let i=0; i<len; i++) {
Promise.resolve(promiseArray[i]).then((res) => {
// arr.push(res);
counter++;
arr[i] = res;
if (counter === len) {
resolve(arr);
}
}).catch(err => reject(err))
}
})
}
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1')
}, 1000)
})
const pro2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('2')
}, 2000)
})
const pro3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('3')
}, 3000)
})
const promise = PromiseAll([pro1, pro2, pro3]).then((res) => {
console.log(res)
}).catch(err => console.log(err))
复制代码
6. Promise.race
Promise.race
会返回一个由所有可迭代实例中第一个fulfilled
或rejected
的实例包装后的新实例。
Promise.race = function(promiseArr) {
return new Promise((resolve, reject) => {
for (let i=0; i<promiseArr.length; i++) {
Promise.resolve(promiseArr[i]).then(res => {
resolve(res)
}).catch(err => reject(err))
}
})
}
复制代码
7. Promise.allSettled
Promise.allSettled()
方法返回一个promise
,该promise
在所有给定的promise
已被解析或被拒绝后解析,并且每个对象都描述每个promise
的结果。
// 如下格式
Promise.allSettled([
Promise.resolve('a'),
Promise.reject('b'),
])
.then(arr => {
console.log(res);
// [
// { status: 'fulfilled', value: 'a' },
// { status: 'rejected', reason: 'b' },
// ]
});
复制代码
function PromiseAllSettled(arr) {
return new Promise((resolve, reject) => {
if (!Array.isArray(arr)) {
throw new Error('必须是一个数组')
}
let count = 0;
let len = arr.length;
let array = [];
for (let i=0; i<len; i++) {
Promise.resolve(arr[i]).then((item) => {
count++;
array[i] = {status: 'fulfilled', value: item};
if (count >= len) {
resolve(array)
}
}).catch((reason) => {
count++;
array[i] = {status: 'rejected', value: reason};
if (count >= len) {
resolve(array)
}
})
}
})
}
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('1')
}, 1000)
})
const pro2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('2')
}, 2000)
})
const pro3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('3')
}, 3000)
})
const promise = PromiseAllSettled([pro1, pro2, pro3]).then((res) => {
console.log(res)
}).catch(err => console.log(err))
复制代码
8. Promise.any
-
该方法接受一组
Promise
实例作为参数,包装成一个新的Promise
实例返回。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。 -
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是Promise.any()
不会因为某个Promise
变成rejected
状态而结束,必须等到所有参数Promise
变成rejected
状态才会结束。
// Promise.any
function PromiseAny(arr) {
return new Promise((resolve, reject) => {
if (!Array.isArray(arr)) {
throw new Error('必须传入一个数组')
}
let len = arr.length;
let count = 0;
let result = [];
for (let i=0; i<len; i++) {
Promise.resolve(arr[i]).then(res => {
resolve(res);
}).catch(err => {
result[i] = err;
count++;
if (count === len) {
reject(`AggregateError: All promises were rejected`)
}
})
}
})
}
// 测试
const pro1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('1')
}, 1000)
})
const pro2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('2')
}, 2000)
})
const pro3 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('3')
}, 3000)
})
const promise = PromiseAny([pro1, pro2, pro3]).then((res) => {
console.log(res)
}).catch(err => console.log(err))
复制代码
6、Ajax的实现
- 使用
Promise
封装Ajax
;
function ajax(method, url, body, headers) {
// 返回一个 Promise
return new Promise((resolve, reject) => {
// 初始化一个 XMLHttpRequest()对象
let xhr = new XMLHttpRequest();
// 调用 open 方法,并传入三个参数:请求方法,url,同步异步
xhr.open(methods, url, true);
// 循环判断请求头,并设置请求头 setRequestHeader()
for(let key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key])
}
}
// 监听 onreadystatechange() 事件,如果 readystate === 4 返回responseText
xhr.onreadystatechange(() => {
if(xhr.readyState == 4) {
if(xhr.status >= '200' && xhr.status <= 300){
resolve(xhr.responeText)
} else {
reject(xhr)
}
}
})
// 调用 send() 方法 传递参数
xhr.send(body)
})
}
复制代码
7、async / await
function asyncToGenerator(generatorFunc) {
// 返回的是一个新的函数
return function() {
// 先调用generator函数 生成迭代器
// 对应 var gen = testG()
const gen = generatorFunc.apply(this, arguments)
// 返回一个promise 因为外部是用.then的方式 或者await的方式去使用这个函数的返回值的
// var test = asyncToGenerator(testG)
// test().then(res => console.log(res))
return new Promise((resolve, reject) => {
// 内部定义一个step函数 用来一步一步的跨过yield的阻碍
// key有next和throw两种取值,分别对应了gen的next和throw方法
// arg参数则是用来把promise resolve出来的值交给下一个yield
function step(key, arg) {
let generatorResult
// 这个方法需要包裹在try catch中
// 如果报错了 就把promise给reject掉 外部通过.catch可以获取到错误
try {
generatorResult = gen[key](arg)
} catch (error) {
return reject(error)
}
// gen.next() 得到的结果是一个 { value, done } 的结构
const { value, done } = generatorResult
if (done) {
// 如果已经完成了 就直接resolve这个promise
// 这个done是在最后一次调用next后才会为true
// 以本文的例子来说 此时的结果是 { done: true, value: 'success' }
// 这个value也就是generator函数最后的返回值
return resolve(value)
} else {
// 除了最后结束的时候外,每次调用gen.next()
// 其实是返回 { value: Promise, done: false } 的结构,
// 这里要注意的是Promise.resolve可以接受一个promise为参数
// 并且这个promise参数被resolve的时候,这个then才会被调用
return Promise.resolve(
// 这个value对应的是yield后面的promise
value
).then(
// value这个promise被resove的时候,就会执行next
// 并且只要done不是true的时候 就会递归的往下解开promise
// 对应gen.next().value.then(value => {
// gen.next(value).value.then(value2 => {
// gen.next()
//
// // 此时done为true了 整个promise被resolve了
// // 最外部的test().then(res => console.log(res))的then就开始执行了
// })
// })
function onResolve(val) {
step("next", val);
},
// 如果promise被reject了 就再次进入step函数
// 不同的是,这次的try catch中调用的是gen.throw(err)
// 那么自然就被catch到 然后把promise给reject掉啦
function onReject(err) {
step("throw", err);
},
)
}
}
step("next");
})
}
}
复制代码
8、数组扁平化
1. 普通递归
var arr = [1,2,3,4,[4,5,[5,6,7]]]
function flatten(arr) {
let array = [];
for (let i=0; i<arr.length; i++) {
if (Array.isArray(arr[i])) {
array = array.concat(flatten(arr[i]));
} else {
array.push(arr[i]);
}
}
return array;
}
const result = flatten(arr);
console.log(result);
复制代码
2. reduce
var arr = [1,2,3,4,[4,5,[5,6,7]]];
function flatten(arr) {
return arr.reduce((prev, cur) => {
return prev.concat(Array.isArray(cur) ? flatten(cur) : cur);
}, [])
}
const result = flatten(arr);
console.log(result);
复制代码
3.toString + split
var arr = [1,2,3,4,[4,5,[5,6,7]]];
function flatten(arr) {
return arr.toString().split(',').map((res)=>{
return Number(res);
})
}
const result = flatten(arr);
console.log(result);
复制代码
4. 调用 ES6 中的 flat
var arr = [1,2,3,4,[4,5,[5,6,7]]];
function flatten(arr) {
return arr.flat(Infinity);
}
const result = flatten(arr);
console.log(result);
复制代码
5. 正则和 JSON 方法共同处理
let arr = [1, [2, [3, [4, 5]]], 6];
function flatten(arr) {
let str = JSON.stringify(arr);
str = str.replace(/(\[|\])/g, '');
str = '[' + str + ']';
return JSON.parse(str);
}
console.log(flatten(arr)); // [1, 2, 3, 4,5]
复制代码
6. 带第二个参数扁平化--记住
// 数组扁平化
const arr = [1, 2, 3, 4, [4, [5, 6, [7, 8, 9]]]];
function flatten(arr, index) {
return index > 0 ? arr.reduce((prev, curr) => {
return prev.concat(Array.isArray(curr) ? flatten(curr, index - 1) : curr);
}, []) : arr.slice();
}
console.log(arr.flat(2));
console.log(flatten(arr, 2));
复制代码
7. 变形
// 数组扁平化
const arr = [1, 2, 3, 4, [4, [5, 6, [7, 8, 9]]]];
function flatten(arr, index) {
let array = [];
if (index > 0) {
for (let i=0; i<arr.length; i++) {
if (Array.isArray(arr[i])) {
array = array.concat(flatten(arr[i], index - 1))
} else {
array.push(arr[i])
}
}
} else {
array = arr.slice();
}
return array;
}
console.log(arr.flat(3));
console.log(flatten(arr, 3));
复制代码
8. 变形2--forEach
// 数组扁平化
const arr = [1, 2, 3, 4, [4, [5, 6, [7, 8, 9]]]];
function flatten(arr, index) {
let array = [];
if (index > 0) {
arr.forEach(item => {
if (Array.isArray(item)) {
array = array.concat(flatten(item, index - 1))
} else {
array.push(item);
}
})
} else {
array = arr.slice();
}
return array;
}
console.log(arr.flat(2));
console.log(flatten(arr, 2));
复制代码
9、instanceof 函数
instanceof
是用来判断A
是否为B
的实例,表达式为:A instanceof B
,如果A
是B
的实例,则返回true
,否则返回false
;instanceof
运算符用来测试一个对象在其原型链中是否存在一个构造函数的prototype
属性;- 不能检测基本数据类型,在原型链上的结果未必准确,不能检测
null,undefined
; - 实现:遍历左边变量的原型链,直到找到右边变量的
prototype
,如果没有找到,返回false
;
function instanceOf(left, right) {
// 获得对象的原型
var proto = left.__proto__;
// 获得类型的原型
var prototype = right.prototype;
while(true) {
if (proto === null) return false;
if (proto === prototype) return true;
proto = proto.__proto__;
}
}
function a() {
this.name = 'dell';
this.age = 18;
}
var newA = new a();
console.log(instanceOf(newA, a))
复制代码
10、数组的各种排序
1. 冒泡排序
var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function bubbleSort(array) {
if (array.length < 2) return array;
for (let i=0; i<array.length; i++) {
for (let j=i+1; j<array.length; j++) {
if (array[j]<array[i]) {
let num = array[i];
array[i] = array[j];
array[j] = num;
}
}
}
return array;
}
let arr = bubbleSort(a);
console.log(arr);
复制代码
2. 快速排序
var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function quickSort(array) {
if (array.length <= 1) return array;
const len = array.length;
const index = Math.floor(len >> 1);
const pivot = array.splice(index, 1)[0]; // 使用 splice 会返回删除的形成数组
const left = [];
const right = [];
for (let i=0; i<len; i++) {
if (array[i] <= pivot) {
left.push(array[i]);
} else if (array[i] > pivot) { // 坑啊 一定要写 else if {}, 不能写 else {}
right.push(array[i]);
}
}
return quickSort(left).concat([pivot], quickSort(right));
}
const arr = quickSort(a);
console.log(arr);
复制代码
3. 插入排序
var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function insertSort(array) {
const len = array.length;
let current;
let prev;
for (let i=1; i<len; i++) {
current = array[i];
prev = i - 1;
while (prev >= 0 && array[prev] > current) {
array[prev + 1] = array[prev];
prev--;
}
array[prev + 1] = current;
}
return array
}
const arr = insertSort(a);
console.log(arr);
复制代码
4. 选择排序
var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function selectSort(array) {
const len = array.length
let temp
let minIndex
for (let i = 0; i < len - 1; i++) {
minIndex = i;
for (let j = i + 1; j < len; j++) {
if (array[j] <= array[minIndex]) {
minIndex = j;
}
}
temp = array[i];
array[i] = array[minIndex];
array[minIndex] = temp;
}
return array;
}
selectSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]
复制代码
5. 堆排序
var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function heap_sort(arr) {
var len = arr.length;
var k = 0;
function swap(i, j) {
var temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
function max_heapify(start, end) {
var dad = start;
var son = dad * 2 + 1;
if (son >= end) return;
if (son + 1 < end && arr[son] < arr[son + 1]) {
son++;
}
if (arr[dad] <= arr[son]) {
swap(dad, son);
max_heapify(son, end);
}
}
for (var i = Math.floor(len / 2) - 1; i >= 0; i--) {
max_heapify(i, len)
}
for (var j = len - 1; j > k; j--) {
swap(0, j);
max_heapify(0, j);
}
return arr;
}
heap_sort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]
复制代码
6. 归并排序
var a = [1, 3, 6, 3, 23, 76, 1, 34, 222, 6, 456, 221];
function mergeSort(array) {
const merge = (right, left) => {
const result = [];
let il = 0;
let ir = 0;
while (il < left.length && ir < right.length) {
if (left[il] < right[ir]) {
result.push(left[il++]);
} else {
result.push(right[ir++]);
}
}
while (il < left.length) {
result.push(left[il++]);
}
while (ir < right.length) {
result.push(right[ir++]);
}
return result;
}
const mergeSort = array => {
if (array.length === 1) { return array }
const mid = Math.floor(array.length / 2);
const left = array.slice(0, mid);
const right = array.slice(mid, array.length);
return merge(mergeSort(left), mergeSort(right));
}
return mergeSort(array);
}
mergeSort(a); // [1, 1, 3, 3, 6, 6, 23, 34, 76, 221, 222, 456]
复制代码
11、柯里化函数
// 递归遍历
function curry(fn, ...args) {
var len = fn.length;
var args = args || [];
return function() {
var newArgs = args.concat(Array.prototype.slice.call(arguments));
if (newArgs.length < len) {
return curry.call(this, fn, ...newArgs)
} else {
return fn.apply(this, newArgs);
}
}
}
function add(a, b, c) {
return a + b + c;
}
var a = curry(add);
console.log(a(1, 2, 3));
复制代码
// ES6
function curry(fn, ...args) {
if (args.length < fn.length) return (...args1) => curry(fn, ...args, ...args1);
else return fn(...args);
}
function add(a, b, c) {
return a + b + c;
}
var a = curry(add);
var b = a(1,2,3);
console.log(b);
复制代码
闭包 JS基础 编程题 (字节)
var foo = function(...args) {
// 要求实现函数体
}
var f1 = foo(1,2,3);
f1.getValue(); // 6 输出是参数的和
var f2 = foo(1)(2,3);
f2.getValue(); // 6
var f3 = foo(1)(2)(3)(4);
f3.getValue(); // 10
复制代码
解答
var foo = function(...args) {
const target = (...args2) => foo(...args, ...args2);
target.getValue = () => args.reduce((p, q) => p + q, 0)
return target
}
var f1 = foo(1,2,3);
console.log(f1.getValue()) // 6 输出是参数的和
var f2 = foo(1)(2, 3);
console.log(f2.getValue()); // 6
var f3 = foo(1)(2)(3)(4);
console.log(f3.getValue()) // 10
复制代码
柯里化变形编程题
完成 combo
函数。它接受任意多个单参函数(只接受一个参数的函数)作为参数,并且返回一个函数。它的作为用:使得类似 f(g(h(a)))
这样的函数调用可以简写为 combo(f, g, h)(a)
。
// ES6 -- reduce
const combo = (...args) => {
args.length && args.reverse()
return prop =>
args.reduce((acc, cur) => {
return cur(acc);
}, prop)
}
// ES5 -- for
function combo () {
const cbs = [...arguments].reverse();
return function () {
var res = cbs[0](...arguments);
for (let i = 1; i < cbs.length; i++) {
res = cbs[i](res);
}
return res;
}
}
/* 以下为测试代码 */
const addOne = (a) => a + 1
const multiTwo = (a) => a * 2
const divThree = (a) => a / 3
const toString = (a) => a + ''
const split = (a) => a.split('')
split(toString(addOne(multiTwo(divThree(666)))))
// => ["4", "4", "5"]
const testForCombo = combo(split, toString, addOne, multiTwo, divThree)
testForCombo(666)
// => ["4", "4", "5"]
复制代码
12、数组去重
1. Set
let arr = [1,2,3,4,4,5,6,7];
[...new Set(arr)]
复制代码
2. Set + Array.from
let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
return Array.from(new Set(arr));
}
复制代码
3. reduce + includes
let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
return arr.reduce((prev, cur) => prev.includes(cur) ? prev : [...prev, cur], []);
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
复制代码
4. filter + indexOf
let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
return arr.filter((item, index, arr) => {
return arr.indexOf(item) === index;
})
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
复制代码
5. for + indexOf
let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
let array = []
for (let i=0; i<arr.length; i++) {
if (arr.indexOf(arr[i]) === i) {
array.push(arr[i]);
}
}
return array;
}
const uniqueArr = unique(arr);
console.log(uniqueArr)
复制代码
6. for + indexOf
let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
let array = [];
for (let i=0; i<arr.length; i++) {
if (array.indexOf(arr[i]) === -1) {
array.push(arr[i]);
}
}
return array;
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
复制代码
7. for + includes
let arr = [1,2,3,4,4,5,6,7];
function unique(arr) {
let array = [];
for (let i=0; i<arr.length; i++) {
if (!array.includes(arr[i])) {
array.push(arr[i])
}
}
return array
}
const uniqueArr = unique(arr);
console.log(uniqueArr);
复制代码
13、发布订阅模式
// 我们以 ES6 类的形式写出来
class EventEmitter {
constructor() {
// 事件对象,存储订阅的type类型
this.events = Object.create(null);
}
/**
* 注册事件监听者
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
on(type, cb) {
// 如果该type类型存在,就继续向数组中添加回调cb
if (this.events[type]) {
this.events[type].push(cb);
} else {
// type类型第一次存入的话,就创建一个数组空间并存入回调cb
this.events[type] = [cb];
}
}
/**
* 发布事件
* @param {String} type 事件类型
* @param {...any} args 参数列表,把emit传递的参数赋给回调函数
*/
emit(type, ...args) {
// 遍历对应type订阅的数组,全部执行
if (this.events[type]) {
this.events[type].forEach(listener => {
listener.call(this, ...args);
});
}
}
/**
* 移除某个事件的一个(相同)监听者
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
off(type, cb) {
if (this.events[type]) {
this.events[type] = this.events[type].filter(listener => {
// 过滤用不到的回调cb
return listener !== cb && listener.listen !== cb;
});
}
if (this.events[type].length === 0) {
delete this.events[type];
}
}
/**
* 移除某个事件的所有监听者
* @param {String} type 事件类型
*/
offAll(type) {
if (this.events[type]) {
delete this.events[type];
}
}
/**
* 只触发一次的发布
* @param {String} type 事件类型
* @param {Function} cb 回调函数
*/
once(type, cb) {
function wrap() {
cb(...arguments);
this.off(type, wrap);
}
// 先绑定,调用后删除
wrap.listen = cb;
// 直接调用on方法
this.on(type, wrap);
}
}
// 测试
// 创建事件管理器实例
const ee = new EventEmitter();
// 注册一个chifan事件监听者
ee.on('chifan', function() { console.log('吃饭了,我们走!') })
// 发布事件chifan
ee.emit('chifan');
// 也可以emit传递参数
ee.on('chifan', function(address, food) { console.log(`吃饭了,我们去${address}吃${food}!`) });
ee.emit('chifan', '三食堂', '铁板饭'); // 此时会打印两条信息,因为前面注册了两个chifan事件的监听者
// 测试移除事件监听
const toBeRemovedListener = function() { console.log('我是一个可以被移除的监听者') };
ee.on('testoff', toBeRemovedListener);
ee.emit('testoff');
ee.off('testoff', toBeRemovedListener);
ee.emit('testoff'); // 此时事件监听已经被移除,不会再有console.log打印出来了
// 测试移除chifan的所有事件监听
ee.offAll('chifan');
console.log(ee); // 此时可以看到ee.listeners已经变成空对象了,再emit发送chifan事件也不会有反应了
复制代码
14、深浅拷贝
1. 浅拷贝
var obj = {a: 1, b: 2, c: { d: 1, e: 5 }}
function shallowClone(target) {
if (typeof target === 'object' && target !== null) {
var obj = Array.isArray(target) ? [] : {};
for (var key in target) {
if (target.hasOwnProperty(key)) {
obj[key] = target[key]
}
}
return obj;
}
// 首层深拷贝 后面都是浅拷贝
var o = shallowClone(obj);
obj.c.d = 2;
console.log(obj); // {a: 1, b: 2, c: {d:2, e:5}}
console.log(o); // {a: 1, b: 2, c: {d:2, e:5}}
复制代码
2. 深拷贝
- 判断类型,正则和日期直接返回新对象
- 空或者非对象类型,直接返回原值
- 考虑循环引用,判断如果
hash
中含有直接返回hash
中的值 - 新建一个相应的
new obj.constructor
加入hash
- 遍历对象递归(普通
key
和key
是symbol
情况)
// 基础版
var obj = {a: 1, b: 2, c: { d: 1, e: 5 }}
function deepClone(target) {
if (typeof target !== 'object' && target !== null) {
throw new Error('请输入一个对象/数组');
}
let obj = Array.isArray(target) ? [] : {};
for (const key in target) {
if (target.hasOwnProperty(key)) {
if (typeof target[key] === 'object') {
obj[key] = deepClone(target[key]);
} else {
obj[key] = target[key];
}
}
}
return obj;
}
var o = deepClone(obj);
obj.c.d = 2;
console.log(obj); // {a: 1, b: 2, c: {d: 2, e: 5}}
console.log(o); // {a: 1, b: 2, c: {d: 1, e: 5}}
复制代码
// 进阶版
function deepClone(obj,hash = new WeakMap()) {
if(obj instanceof RegExp) return new RegExp(obj);
if(obj instanceof Date) return new Date(obj);
if(obj === null || typeof obj !== 'object') return obj;
// 循环引用的情况;
if(hash.has(obj)) {
return hash.get(obj)
}
// new 一个相应的对象;
// obj 为 Array,相当于 new Array();
// obj 为 Object,相当于 new Object();
let constr = new obj.constructor();
hash.set(obj,constr);
for(let key in obj) {
if(obj.hasOwnProperty(key)) {
constr[key] = deepClone(obj[key],hash)
}
}
// 考虑 symbol 的情况;
let symbolObj = Object.getOwnPropertySymbols(obj);
for(let i=0;i<symbolObj.length;i++){
if(obj.hasOwnProperty(symbolObj[i])) {
constr[symbolObj[i]] = deepClone(obj[symbolObj[i]],hash)
}
}
return constr
}
复制代码
15、实现 sleep 方法
1. 回调
function sleep(callback, time) {
setTimeout(callback, time);
}
function sayHi() {
console.log('satHi');
}
sleep(sayHi, 1000);
复制代码
2. promise
// 简单版
function sleep(time) {
return new Promise((resolve, reject) => setTimeout(resolve, time));
}
sleep(1000).then(() => { console.log('sayHi') });
复制代码
// 进阶版
function sleep(fn, time, ...args) {
return new Promise((resolve, reject) => {
setTimeout(() => {
// reslove(fn(...args))
Promise.resolve(fn(...args)).then(resolve).catch(reject);
}, time)
})
}
复制代码
3. Generator
function* sleep(time) {
yield new Promise(function(resolve, reject) {
setTimeout(resolve, time);
})
}
sleep(1000).next().value.then(()=>{console.log('sayHi')});
复制代码
4. async / await
function sleep(time) {
return new Promise((resolve, reject) => {
setTimeout(resolve, time);
})
}
aysnc function output(time) {
const result = await sleep(time);
console.log('sayHi');
return result;
}
output(1000)
复制代码
16、实现 Object.create()
MDN
文档;Object.create()
会将参数对象作为一个新创建的空对象的原型, 并返回这个空对象;
// 简单版
function myCreate(obj) {
// 新声明一个函数
function C() {};
// 将函数的原型指向obj
C.prototype = obj;
// 返回这个函数的实力化对象
return new C();
}
// 官方版 Polyfill
if (typeof Object.create !== "function") {
Object.create = function (proto, propertiesObject) {
if (typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object: ' + proto);
} else if (proto === null) {
throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
}
if (typeof propertiesObject !== 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");
function F() {}
F.prototype = proto;
return new F();
};
}
复制代码
17、实现 Object.assign
Object.assign2 = function(target, ...source) {
if (target === null || target === undefined) {
throw new TypeError('Cannot convert undefined or null to object')
}
let ret = Object(target);
source.forEach(function(obj) {
if (obj !== null || obj !== undefined) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
ret[key] = obj[key];
}
}
}
})
return ret;
}
复制代码
18、实现每隔一秒打印(闭包)
for (var i=1; i<=5; i++) {
(function (i) {
setTimeout(() => console.log(i), 1000*i);
})(i)
}
for (var i=1; i<=5; i++) {
setTimeout(function(i) {
console.log(i);
}.bind(null, i), 1000*i)
}
复制代码
19、手写一个 jsonp
let jsonp = function (url, data = {}, callback='callback') {
// 转化数据为 url 字符串形式
let dataStr = url.indexOf('?') === -1 ? '?' : '&'
for(let key in data) {
dataStr += `${key}=${data[key]}&`;
}
// 处理url中的回调函数
dataStr += 'callback=' + callback;
// 创建 srcipt 标签并添加 src 属性值
let scriptBody = document.createElement('script')
scriptBody.src = url + dataStr
// append 到页面中添加到页面就立刻发起请求
document.body.appendChild(scriptBody);
// 返回一个 promise
return new Promise((resolve, reject) => {
window[callback] = (data) => {
try {
resolve(data)
} catch(e) {
reject(e)
} finally {
// 移除 script 元素
scriptBody.parentNode.removeChild(scriptBody);
console.log(scriptBody);
}
}
})
}
jsonp('https://photo.sina.cn/aj/index', {page:1,cate:'recommend'}).then(res=>{console.log(res)})
复制代码
20、手写观察者模式
在观察者模式中,只有两个主体,分别是目标对象 Subject
,观察者 Observer
。
- 观察者需
Observer
要实现update
方法,供目标对象调用。update
方法中可以执行自定义的业务代码。 - 目标对象
Subject
也通常被叫做被观察者或主题,它的职能很单一,可以理解为,它只管理一种事件。Subject
需要维护自身的观察者数组observerList
,当自身发生变化时,通过调用自身的notify
方法,依次通知每一个观察者执行update
方法。
// 观察者
class Observer {
/**
* 构造器
* @param {Function} cb 回调函数,收到目标对象通知时执行
*/
constructor(cb){
if (typeof cb === 'function') {
this.cb = cb;
} else {
throw new Error('Observer 构造器必须传入函数类型!');
}
}
/**
* 被目标对象通知时执行
*/
update() {
this.cb();
}
}
// 目标对象
class Subject {
constructor() {
// 维护观察者列表
this.observerList = [];
}
/**
* 添加一个观察者
* @param {Observer} observer Observer实例
*/
addObserver(observer) {
this.observerList.push(observer);
}
/**
* 通知所有的观察者
*/
notify() {
this.observerList.forEach(observer => {
observer.update();
})
}
}
const observerCallback = function() {
console.log('我被通知了');
}
const observer = new Observer(observerCallback);
const subject = new Subject();
subject.addObserver(observer);
subject.notify();
复制代码
21、获取对象内容
完成 deepGet
函数,给它传入一个对象和字符串,字符串表示对象深层属性的获取路径,可以深层次获取对象内容:
const deepGet = (obj, prop) => {
const keyArr = prop.split('.').map(key => key);
const reducer = (acc, cur) => {
const objKey = cur.includes('[') && cur.replaceAll(/[\[?0-9\]$]/gi, '');
if (Array.isArray(acc[objKey])) {
cur = cur.replaceAll(/[^?0-9]/gi, '');
return acc[objKey][cur] || {};
}
return acc[cur] ? acc[cur] : {};
}
const result = keyArr.reduce(reducer, obj);
return Object.keys(result).length ? result : undefined;
}
/** 以下为测试代码 */
deepGet({
school: {
student: { name: 'Tomy' },
},
}, 'school.student.name') // => 'Tomy'
deepGet({
school: {
students: [
{ name: 'Tomy' },
{ name: 'Lucy' },
],
}
}, 'school.students[1].name') // => 'Lucy'
// 对于不存在的属性,返回 undefined
deepGet({ user: { name: 'Tomy' } }, 'user.age'); // => undefined
deepGet({ user: { name: 'Tomy' } }, 'school.user.age'); // => undefined
复制代码
22、生成随机数的各种方法?
// 生成 [min, max) 的随机数, 其中 Math.random() 是生成 [0, 1) 的随机数
function getRandom(min, max) {
return Math.floor(Math.random() * (max - min)) + min
}
// 生成[min, max] 的随机数
function getRandom(min, max) {
return Math.floor(Math.random() * (max + 1 - min)) + min
}
// 生成 (min, max) 的随机数
function getRandom(min, max) {
let result = Math.random()*(m-n)+n;
while(result == n) {
result = Math.random()*(m-n)+n;
}
return result;
}
// 生成 (min, max] 的随机数
function getRandom(min, max) {
let result = Math.random()*(m-n+1)+n-1;
while(result<n) {
result = Math.random()*(m-n+1)+n-1;
}
return result;
}
复制代码
23、如何实现数组的随机排序?
let arr = [2,3,454,34,324,32];
arr.sort(randomSort);
function randomSort(a, b) {
return Math.random() > 0.5 ? -1 : 1;
}
复制代码
24、实现正则切分千分位
// 无小数点
let num1 = '1321434322222'
num1.replace(/(\d)(?=(\d{3})+$)/g,'$1,');
// 有小数点
let num2 = '342243242322.3432423';
num2.replace(/(\d)(?=(\d{3})+\.)/g,'$1,');
复制代码
25、解析 URL Params 为对象
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
parseParam(url);
/* 结果
{
user: 'anonymous',
id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/
复制代码
function parseParam(url) {
const paramsStr = /.+\?(.+)$/.exec(url)[1]; // 将 ? 后面的字符串取出来
const paramsArr = paramsStr.split('&'); // 将字符串以 & 分割后存到数组中
let paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
if (/=/.test(param)) { // 处理有 value 的参数
let [key, val] = param.split('='); // 分割 key 和 value
val = decodeURIComponent(val); // 解码
val = /^\d+$/.test(val) ? parseFloat(val) : val; // 判断是否转为数字
if (paramsObj.hasOwnProperty(key)) { // 如果对象有 key,则添加一个值
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
复制代码
26、模板引擎实现
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
复制代码
// 方法一
function render(template, data) {
const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
if (reg.test(template)) { // 判断模板里是否有模板字符串
const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
return render(template, data); // 递归的渲染并返回渲染后的结构
}
return template; // 如果模板没有模板字符串直接返回
}
复制代码
// 方法二(推荐)
function render(template, data) {
let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {
return data[key];
});
return computed;
}
复制代码
27、转化为驼峰命名
var s1 = "get-element-by-id";
// 转化为 getElementById
复制代码
var f = function(s) {
return s.replace(/-\w/g, function(x) {
return x.slice(1).toUpperCase();
})
}
复制代码
28、查找字符串中出现最多的字符和个数
- 例: abbcccddddd -> 字符最多的是d,出现了5次
let str = "abcabcabcbbccccc";
let num = 0;
let char = '';
// 使其按照一定的次序排列
str = str.split('').sort().join('');
// "aaabbbbbcccccccc"
// 定义正则表达式
let re = /(\w)\1+/g;
str.replace(re,($0,$1) => {
if(num < $0.length) {
num = $0.length;
char = $1;
}
});
console.log(`字符最多的是${char},出现了${num}次`);
复制代码
29、图片懒加载
let imgList = [...document.querySelectorAll('img')]
let length = imgList.length
const imgLazyLoad = function() {
let count = 0
return (function() {
let deleteIndexList = []
imgList.forEach((img, index) => {
let rect = img.getBoundingClientRect()
if (rect.top < window.innerHeight) {
img.src = img.dataset.src;
deleteIndexList.push(index);
count++;
if (count === length) {
document.removeEventListener('scroll', imgLazyLoad);
}
}
})
imgList = imgList.filter((img, index) => !deleteIndexList.includes(index));
})()
}
// 这里最好加上防抖处理
document.addEventListener('scroll', imgLazyLoad);
复制代码
30、实现 reduce、forEach
// reduce
Array.prototype.reduce = function(fn, val) {
// 很好理解,就判断 val 是否有传入值
for (let i = 0; i < this.length; i++) {
// 没有传入值
if (typeof val === 'undefined') {
val = this[i];
} else { // 有传入 val 值
// total就是初始值 val,之后的依次传入对应
val = fn(val, this[i], i, this);
}
}
return val;
};
复制代码
// forEach
// 基础版本
Array.prototype.forEach = function(callback) {
var len = this.length;
for(var i = 0; i < len; i++) {
callback(this[i], i, this);
}
}
// call 实现
Array.prototype.forEach = function(callback, thisArg) {
var len = this.length;
for(var i = 0; i < len; i++) {
// callback(this[i], i, this);
callback.call(thisArg, this[i], i, this);
}
}
// bind 实现
Array.prototype.forEach = function(callback, thisArg) {
var len = this.length;
callback = callback.bind(thisArg);
for(var i = 0; i < len; i++) {
callback(this[i], i, this);
}
}
复制代码
31、用 setTimeout 实现 setInterval
let timer = null;
function mockSetInterval(fn, delay, ...args) {
let recur = function() {
timer = setTimeout(() => {
fn.apply(this, args);
recur();
}, delay)
}
recur();
}
function mockClearInterval(id) {
clearTimeout(id);
}
mockSetInterval((a, b) => {console.log(a, b)}, 1000, '1', '2')
setTimeout(() => {
mockClearInterval(timer);
}, 4000)
复制代码
32、实现 chunk 函数
- 根据 size 值,将数组划分为几段,例如,arr = [1,2,3,4,5,6,7], size = 2;
- 返回:target = [[1,2], [3,4], [5,6], [7]]
function chunk(arr, size) {
let target = [];
let count = size;
if (arr.length < size) {
target.push(arr.slice());
} else {
let len = arr.length - arr.length % size;
for (let i=0; i<len; i=i+size) {
let newArr = arr.slice(i, count);
count = count + size;
target.push(newArr);
}
if (arr.length % size !== 0) {
target.push(arr.slice(arr.length - arr.length % size));
}
}
return target;
}
let target = chunk([1,2,3,4,5,6], 7);
console.log(target);
复制代码
33、实现红绿灯
- 要求使用一个 div 实现红绿灯效果,把一个圆形 div 按照绿色 3s,黄色 1s,红色 2s 这样循环改变颜色
#traffic-color {
width: 100px;
height: 100px;
overflow: hidden;
border-radius: 50%;
}
复制代码
<div id="traffic-color"></div>
const TRAFFIC_COLOR_CONFIG = {
'green': 3000,
'yellow': 1000,
'red': 2000
}
function delay(duration) {
return new Promise((resolve, reject) => setTimeout(resolve, duration));
}
async function changeColor(color) {
document.getElementById('traffic-color').style.background = color;
await delay(TRAFFIC_COLOR_CONFIG[color]);
}
async function run() {
// while(1) {
// await changeColor('green', 3000);
// await changeColor('yellow', 1000);
// await changeColor('red', 2000);
// }
// await changeColor('green', 3000);
// await changeColor('yellow', 1000);
// await changeColor('red', 2000);
for (const key in TRAFFIC_COLOR_CONFIG) {
await changeColor(key);
}
run()
}
run();
复制代码
34、实现 indexOf()
注意几点:
- 如果索引开始大于字符串/数组的长度时,返回 -1;
- 如果索引开始小于等于 0,则按照从 0 开始索引;
- 如果字符串或者数组长度为 0,返回 0;
- 否则,返回 -1;
function indexOf(searchValue, index=0) {
if (this.length < 1 || index > this.length) return -1;
if (!searchValue) return 0;
index = index <= 0 ? 0 : index;
for (let i=0; i<this.length; i++) {
if (this[i] === searchValue) {
return i;
}
}
return -1;
}
Array.prototype.myIndexOf = indexOf;
String.prototype.myIndexOf = indexOf;
console.log([1, 2, 3].myIndexOf(2));
console.log('123'.myIndexOf('2'));
复制代码
35、写版本号排序的方法
- 题目描述:有一组版本号如下['0.1.1', '2.3.3', '0.302.1', '4.2', '4.3.5', '4.3.4.5']。现在需要对其进行排序;
- 排序的结果为 ['4.3.5', '4.3.4.5', '2.3.3', '0.302.1', '0.1.1'];
arr.sort((a, b) => {
let i = 0;
const arr1 = a.split(".");
const arr2 = b.split(".");
while (true) {
const s1 = arr1[i];
const s2 = arr2[i];
i++;
if (s1 === undefined || s2 === undefined) {
return arr2.length - arr1.length;
}
if (s1 === s2) continue;
return s2 - s1;
}
});
console.log(arr);
复制代码
36、类数组转化为数组的方法
- 如何把类数组转化为数组?
const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)
复制代码
37、实现 Object.is()
- Object.is不会转换被比较的两个值的类型,这点和 === 更为相似,他们之间也存在一些区别。
-
- NaN 在 === 中是不相等的,而在 Object.is() 中是相等的;
-
- +0 和 -0 在 === 中是相等的,而在 Object.is() 中是不相等的;
-
Object.is = function (x, y) {
if (x === y) {
// 当前情况下,只有一种情况是特殊的,即 +0 -0
// 如果 x !== 0,则返回true
// 如果 x === 0,则需要判断+0和-0,则可以直接使用 1/+0 === Infinity 和 1/-0 === -Infinity来进行判断
return x !== 0 || 1 / x === 1 / y;
}
// x !== y 的情况下,只需要判断是否为NaN,如果x!==x,则说明x是NaN,同理y也一样
// x和y同时为NaN时,返回true
return x !== x && y !== y;
};
复制代码
38、虚拟 DOM 转化为真实 DOM
JSON
格式的虚拟DOM
怎么转换成真实DOM
;
题目描述:
{
tag: 'DIV',
attrs:{
id:'app'
},
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
}
把上诉虚拟 `DOM` 转化成下方真实 `DOM`;
<div id="app">
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>
复制代码
实现代码如下:
// 真正的渲染函数
function _render(vnode) {
// 如果是数字类型转化为字符串
if (typeof vnode === "number") {
vnode = String(vnode);
}
// 字符串类型直接就是文本节点
if (typeof vnode === "string") {
return document.createTextNode(vnode);
}
// 普通DOM
const dom = document.createElement(vnode.tag);
if (vnode.attrs) {
// 遍历属性
Object.keys(vnode.attrs).forEach((key) => {
const value = vnode.attrs[key];
dom.setAttribute(key, value);
});
}
// 子数组进行递归操作
vnode.children.forEach((child) => dom.appendChild(_render(child)));
return dom;
}
复制代码
39、DOM 节点输出 JSON 的格式
题目描述:
<div>
<span>
<a></a>
</span>
<span>
<a></a>
<a></a>
</span>
</div>
把上诉 `DOM` 结构转成下面的 `JSON` 格式
{
tag: 'DIV',
children: [
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] }
]
},
{
tag: 'SPAN',
children: [
{ tag: 'A', children: [] },
{ tag: 'A', children: [] }
]
}
]
}
复制代码
实现代码如下:
function dom2Json(domtree) {
let obj = {};
obj.name = domtree.tagName;
obj.children = [];
domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
return obj;
}
复制代码
40、实现一个对象的 flatten 方法
题目描述:
const obj = {
a: {
b: 1,
c: 2,
d: {e: 5}
},
b: [1, 3, {a: 2, b: 3}],
c: 3
}
flatten(obj) 结果返回如下
// {
// 'a.b': 1,
// 'a.c': 2,
// 'a.d.e': 5,
// 'b[0]': 1,
// 'b[1]': 3,
// 'b[2].a': 2,
// 'b[2].b': 3
// c: 3
// }
复制代码
实现代码如下:
function isObject(val) {
return typeof val === "object" && val !== null;
}
function flatten(obj) {
if (!isObject(obj)) {
return;
}
let res = {};
const dfs = (cur, prefix) => {
if (isObject(cur)) {
if (Array.isArray(cur)) {
cur.forEach((item, index) => {
dfs(item, `${prefix}[${index}]`);
});
} else {
for (let k in cur) {
dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
}
}
} else {
res[prefix] = cur;
}
};
dfs(obj, "");
return res;
}
flatten();
复制代码
41、列表转成树形结构
题目描述:
[
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
...
]
转成
[
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id: 2,
text: '节点1_1',
parentId: 1
}
]
}
]
复制代码
实现代码如下:
function listToTree(data) {
let temp = {};
let treeData = [];
for (let i = 0; i < data.length; i++) {
temp[data[i].id] = data[i];
}
for (let i in temp) {
if (+temp[i].parentId != 0) {
if (!temp[temp[i].parentId].children) {
temp[temp[i].parentId].children = [];
}
temp[temp[i].parentId].children.push(temp[i]);
} else {
treeData.push(temp[i]);
}
}
return treeData;
}
复制代码
42、树形结构转成列表
题目描述:
[
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id:2,
text: '节点1_1',
parentId:1
}
]
}
]
转成
[
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
...
]
复制代码
实现代码如下:
function treeToList(data) {
let res = [];
const dfs = (tree) => {
tree.forEach((item) => {
if (item.children) {
dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}
复制代码
43、深度优先遍历
深度优先遍历——是指从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点的第一个未被访问的邻结点,然后再以此邻结点为顶点,继续找它的下一个顶点进行访问。重复此步骤,直至所有结点都被访问完为止。
深度优先遍历的递归写法
function deepTraversal(node) {
let nodes = []
if (node != null) {
nodes.push(node)
let childrens = node.children
for (let i = 0; i < childrens.length; i++) {
deepTraversal(childrens[i])
}
}
return nodes
}
复制代码
深度优先遍历的非递归写法
function deepTraversal(node) {
let nodes = []
if (node != null) {
let stack = []
// 同来存放将来要访问的节点
stack.push(node)
while (stack.length != 0) {
let item = stack.pop()
// 正在访问的节点
nodes.push(item)
let childrens = item.children
for ( let i = childrens.length - 1; i >= 0; i--) {
// 将现在访问点的节点的子节点存入 stack,供将来访问 )
stack.push(childrens[i])
}
}
}
return nodes
}
复制代码
44、广度优先遍历
广度优先遍历——是从某个顶点出发,首先访问这个顶点,然后找出刚访问这个结点所有未被访问的邻结点,访问完后再访问这些结点中第一个邻结点的所有结点,重复此方法,直到所有结点都被访问完为止。
广度优先遍历的递归写法
function wideTraversal(node) {
let nodes = [], i = 0
if (node != null) {
nodes.push(node)
wideTraversal(node.nextElementSibling)
node = nodes[i++]
wideTraversal(node.firstElementChild)
}
return nodes
}
复制代码
广度优先遍历的非递归写法
function wideTraversal(node) {
let nodes = [], i = 0
while (node != null) {
nodes.push(node)
node = nodes[i++]
let childrens = node.children
for (let i = 0; i < childrens.length; i++) {
nodes.push(childrens[i])
}
}
return nodes
}
复制代码