盘点那些 JS 手写题
- 盘点那些 JS 手写题
- 1. JS 基础
- 1. 手写 Object.create
- 2. 手写 instanceof 方法
- 3. 手写 new 操作符
- 4. 手写一个通用的类型检测方法
- 5. 手写 call 函数
- 6. 手写 apply 函数
- 7. 手写 bind 函数
- 8. 手写 Array.isArray
- 9. 手写数组的 flat 方法
- 10. 手写数组的 push 方法
- 11. 手写数组的 filter方法
- 12. 手写数组的 map 方法
- 13. 手写字符串的 repeat 方法
- 14. 手写 Promise
- 15. 手写 Promise.all
- 16. 手写 Promise.race
- 17. 手写 Promise.any
- 18. 手写 Promise.allSettled
- 2. 场景应用
盘点那些 JS 手写题
1. JS 基础
1. 手写 Object.create
语法
// 返回一个新对象,带着指定的原型对象和属性。
Object.create(proto,[propertiesObject])
proto
:新创建对象的原型对象,必须为null
或者原始包装对象,否则会抛出异常propertiesObject
:可选参数,需要是一个对象,该传入对象的自有可枚举属性(即其自身定义的属性,而不是其原型链上的枚举属性)将为新创建的对象添加指定的属性值和对应的属性描述符
实现
function createObject(proto, propertiesObject = {}) {
// 类型检验
if (!['object', 'function'].includes(typeof proto)) {
throw new TypeError("proto必须为对象或者函数");
}
function f() {
for(const key in propertiesObject) {
if (propertiesObject[key]['enumerable']) {
this[key] = propertiesObject[key]['value'];
}
}
}
f.prototype = proto
return new f();
}
2. 手写 instanceof 方法
语法
object instanceof constructor
实现
function myInstanceof(obj, objType) {
// 首先用typeof来判断基础数据类型,如果是,直接返回false
if (obj === null || typeof obj !== 'object') return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(obj);
//循环往下寻找,直到找到相同的原型对象
while(proto !== null) {
//找到相同原型对象,返回true
if (proto === objType.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
3. 手写 new 操作符
语法
new constructor[([arguments])]
constructor
:一个指定对象实例的类型的类或函数。arguments
:一个用于被constructor
调用的参数列表。
描述
new
关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即
{}
); - 为步骤1新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
实现
function myNew() {
const constructor = Array.prototype.shift.call(arguments);
// 判断第一个参数是否为构造函数
if (typeof constructor !== 'function') {
throw new TypeError('第一个参数不是构造函数');
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
const newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
const result = constructor.call(newObject, arguments);
// 判断返回结果
return result && ['object', 'function'].includes(typeof result) ? result : newObject;
}
4. 手写一个通用的类型检测方法
实现
function getType(obj) {
const type = typeof obj;
// 如果是基本数据类型或函数对象,直接返回
if (type !== 'object') {
return type;
}
// 对于typeof返回结果是 object 的,再进行如下的判断
const result = Object.prototype.toString.call(obj).split(' ')[1].replace(']', '');
return result;
}
5. 手写 call 函数
语法
function.call(thisArg, arg1, arg2, ...)
thisArg
:可选的。在function
函数运行时使用的this
值。请注意,this
可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为null
或undefined
时会自动替换为指向全局对象,原始值会被包装。arg1, arg2, ...
:指定的参数列表。- 返回值:使用调用者提供的
this
值和参数调用该函数的返回值。若该方法没有返回值,则返回undefined
。
实现
Function.prototype.myCall = function(context) {
// 判断调用对象
if (typeof this !== 'function') {
throw new TypeError('该对象不存在 myCall 方法');
}
const args = [...arguments].slice(1);
// 判断 context 是否传入,如果未传入则设置为 window
context = context ? Object(context) : window;
// 给context新增一个独一无二的属性以免覆盖原有属性
const key = Symbol();
// 将调用函数设置成对象的方法
context[key] = this;
// 通过隐式绑定的方式调用函数
const result = context[key](...args);
// 将属性删除
delete context[key];
return result;
}
6. 手写 apply 函数
语法
func.apply(thisArg, [argsArray])
thisArg
:必选的。在func
函数运行时使用的this
值。请注意,this
可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为null
或undefined
时会自动替换为指向全局对象,原始值会被包装。argsArray
:可选的。一个数组或者类数组对象,其中的数组元素将作为单独的参数传给func
函数。如果该参数的值为null
或undefined
,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。- 返回值:调用有指定
this
值和参数的函数的结果。
实现
Function.prototype.myApply = function(context) {
// 判断调用对象
if (typeof this !== 'function') {
throw new TypeError('该对象不存在 myApply 方法');
}
// 判断 context 是否存在,如果未传入则为 window
context = context ? Object(context) : global;
// 给context新增一个独一无二的属性以免覆盖原有属性
const key = Symbol();
// 将调用函数设置成对象的方法
context[key] = this;
let result = null;
// 通过隐式绑定的方式调用函数
if (arguments[1]) {
result = context[key](...arguments[1]);
} else {
result = context[key]();
}
// 将属性删除
delete context[key];
return result;
}
7. 手写 bind 函数
语法
function.bind(thisArg[, arg1[, arg2[, ...]]])
thisArg
:调用绑定函数时作为this
参数传递给目标函数的值。 如果使用new
运算符构造绑定函数,则忽略该值。当使用bind
在setTimeout
中创建一个函数(作为回调提供)时,作为thisArg
传递的任何原始值都将转换为object
。如果bind
函数的参数列表为空,或者thisArg
是null
或undefined
,执行作用域的this
将被视为新函数的thisArg
。arg1, arg2, ...
:当目标函数被调用时,被预置入绑定函数的参数列表中的参数。- 返回值:返回一个原函数的拷贝,并拥有指定的
this
值和初始参数。
Function.prototype.myBind = function (context) {
// 获取参数
let args = [...arguments].slice(1);
const fn = this;
// 判断 context 是否存在,如果未传入则为 window
context = context ? Object(context) : global;
function newFn() {
// 判断是否 new 调用
if (this instanceof newFn) {
return new fn(...args, ...arguments)
}
return fn.apply(context, [...args,...arguments])
}
if (fn.prototype) {
// 复制源函数的 prototype 给 newFn 一些情况下函数没有prototype,比如箭头函数
newFn.prototype = Object.create(fn.prototype);
}
return newFn;
}
8. 手写 Array.isArray
语法
Array.isArray(obj)
Array.isArray()
用于确定传递的值是否是一个 Array
。
实现
Array.myIsArray = function (obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
9. 手写数组的 flat 方法
flat()
方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。
语法
var newArray = arr.flat([depth])
depth
:可选。指定要提取嵌套数组的结构深度,默认值为 1。- 注 使用
Infinity
,可展开任意深度的嵌套数组
- 注 使用
- 返回值:一个包含将数组与子数组中所有元素的新数组。
实现
// 递归
function myFlat(arr, depth = 1) {
if (!Array.isArray(arr) || depth <= 0) {
return arr;
}
return arr.reduce((prev, cur) => {
if (Array.isArray(cur)) {
return prev.concat(myFlat(cur, depth - 1));
}
return prev.concat(cur);
}, []);
}
// 非递归
function myFlat(arr, depth = 1) {
let result = arr;
while(depth > 0) {
let hasArray = false;
result = result.reduce((prev, cur) => {
if (Array.isArray(cur)) {
hasArray = true;
}
return prev.concat(cur);
}, []);
if (!hasArray) break;
depth --;
}
return result;
}
10. 手写数组的 push 方法
语法
arr.push(element1, ..., elementN)
elementN
:被添加到数组末尾的元素- 返回值:当调用该方法时,新的
length
属性值将被返回。
实现
Array.prototype.myPush = function() {
for(let i = 0; i < arguments.length; i++) {
this[this.length] = arguments[i];
// 处理类数组的长度
if (!Array.isArray(this)) {
this.length ++;
}
}
return this.length;
}
11. 手写数组的 filter方法
语法
var newArray = arr.filter(callback(element[, index[, array]])[, thisArg])
callback
:用来测试数组的每个元素的函数。返回true
表示该元素通过测试,保留该元素,false
则不保留。它接受以下三个参数:element
:数组中当前正在处理的元素。index
:可选。正在处理的元素在数组中的索引。array
:可选。调用了filter
的数组本身。
thisArg
:可选。执行callback
时,用于this
的值。
实现
2Array.prototype.myFilter = function(fn, thisArg) {
// 判断第一个参数是否为函数
if (typeof fn !== 'function') {
throw new TypeError('fn 不是一个函数');
}
// 确定回调函数的 this 指向
let context = thisArg || window;
const result = [];
for(let i = 0; i < this.length; i++) {
if (fn.call(context, this[i], i, this)) {
result.push(this[i]);
}
}
return result;
}
12. 手写数组的 map 方法
语法
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
callback
:生成新数组元素的函数,使用三个参数:currentValue
:callback
数组中正在处理的当前元素。index
:可选。callback
数组中正在处理的当前元素的索引。array
:可选。map
方法调用的数组。
thisArg
:可选。执行callback
函数时值被用作this
。- 返回值:一个由原数组每个元素执行回调函数的结果组成的新数组。
实现
Array.prototype.myMap = function(fn, thisArg) {
// 判断第一个参数是否为函数
if (typeof fn !== 'function') {
throw new TypeError('fn 不是一个函数');
}
// 确定回调函数的 this 指向
let context = thisArg || window;
const result = [];
for(let i = 0; i < this.length; i++) {
result.push(fn.call(context, this[i], i, this));
}
return result;
}
13. 手写字符串的 repeat 方法
语法
str.repeat(count)
count
:介于0
和+Infinity
之间的整数。表示在新构造的字符串中重复了多少遍原字符串。- 返回值:包含指定字符串的指定数量副本的新字符串。
实现
String.prototype.myRepeat = function(count) {
if (count < 0) {
throw new RangeError('repeat count must be non-negative');
}
if (count === Infinity) {
throw new RangeError('repeat count must be less than infinity');
}
let result = '';
for(let i = 0; i < count; i++) {
result += this;
}
return result;
}
14. 手写 Promise
15. 手写 Promise.all
语法
Promise.all(iterable);
- iterable:一个可迭代对象,如
Array
或String
。 - 返回值
- 如果传入的参数是一个空的可迭代对象,则返回一个**已完成(already resolved)**状态的
Promise
。 - 如果传入的参数不包含任何
promise
,则返回一个异步完成(asynchronously resolved)Promise
。注意:Google Chrome 58 在这种情况下返回一个**已完成(already resolved)**状态的Promise
。 - 其它情况下返回一个处理中(pending)的
Promise
。这个返回的promise
之后会在所有的promise
都完成或有一个promise
失败时异步地变为完成或失败。 返回值将会按照参数内的promise
顺序排列,而不是由调用promise
的完成顺序决定。
- 如果传入的参数是一个空的可迭代对象,则返回一个**已完成(already resolved)**状态的
实现
function promiseAll(promiseList) {
return new Promise((resolve, reject) => {
if(!Array.isArray(promiseList)){
throw new TypeError(`argument must be a array`)
}
let resolvedCounter = 0;
const result = [];
for(let i = 0; i < promiseList.length; i++) {
Promise.resolve(promiseList[i]).then(value => {
resolvedCounter ++;
result[i] = value;
if (resolvedCounter === promiseList.length) {
resolve(result);
}
}, reason => {
reject(reason);
})
}
})
}
16. 手写 Promise.race
语法
Promise.race(iterable);
- iterable:可迭代对象,类似
Array
- 返回值:一个待定的
Promise
只要给定的迭代中的一个promise解决或拒绝,就采用第一个promise的值作为它的值,从而异步地解析或拒绝(一旦堆栈为空)。
实现
function promiseRace(promiseList) {
return new Promise((resolve, reject) => {
for (let i = 0, len = promiseList.length; i < len; i++) {
promiseList[i].then(resolve, reject)
}
})
}
17. 手写 Promise.any
语法
Promise.any(iterable);
iterable
:一个可迭代的对象, 例如 Array。- 返回值
- 如果传入的参数是一个空的可迭代对象,则返回一个 已失败(already rejected) 状态的
Promise
。 - 如果传入的参数不包含任何
promise
,则返回一个 异步完成 (asynchronously resolved)的Promise
。 - 其他情况下都会返回一个处理中(pending) 的
Promise
。 只要传入的迭代对象中的任何一个promise
变成成功(resolve)状态,或者其中的所有的promises
都失败,那么返回的promise
就会 异步地(当调用栈为空时) 变成成功/失败(resolved/reject)状态。
- 如果传入的参数是一个空的可迭代对象,则返回一个 已失败(already rejected) 状态的
实现
function promiseAny(promiseList) {
return new Promise((resolve, reject) => {
if(!Array.isArray(promiseList)){
throw new TypeError(`argument must be a array`)
}
let rejectedCounter = 0;
const result = [];
for(let i = 0; i < promiseList.length; i++) {
Promise.resolve(promiseList[i]).then(resolve, reason => {
result[i] = reason;
rejectedCounter ++;
if (rejectedCounter === promiseList.length) reject(new Error('All promises were rejected'));
})
}
})
}
18. 手写 Promise.allSettled
Promise.allSettled(iterable);
iterable
:一个可迭代的对象,例如Array
,其中每个成员都是Promise
。- 返回值:一旦所指定的 promises 集合中每一个 promise 已经完成,无论是成功的达成或被拒绝,未决议的
Promise
将被异步完成。那时,所返回的 promise 的处理器将传入一个数组作为输入,该数组包含原始 promises 集中每个 promise 的结果。对于每个结果对象,都有一个status
字符串。如果它的值为fulfilled
,则结果对象上存在一个value
。如果值为rejected
,则存在一个reason
。value(或 reason )反映了每个 promise 决议(或拒绝)的值。
实现
function promiseAllSettled(promiseList) {
return new Promise((resolve, reject) => {
if(!Array.isArray(promiseList)){
throw new TypeError(`argument must be a array`)
}
let counter = 0;
const result = [];
for(let i = 0; i < promiseList.length; i++) {
Promise.resolve(promiseList[i]).then(value => {
result[i] = { status: 'fulfilled', value };
counter ++;
if (counter === promiseList.length) resolve(result);
}, reason => {
result[i] = { status: 'rejected', reason };
counter ++;
if (counter === promiseList.length) resolve(result);
})
}
})
}
2. 场景应用
1. 手写防抖函数
函数防抖是指在事件被触发 n 秒后再执行回调,如果在这 n 秒内事件又被触发,则重新计时。这可以使用在一些点击请求的事件上,避免因为用户的多次点击向后端发送多次请求。
function debounce(fn, wait) {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn(...args);
}, wait);
}
}
2. 手写节流函数
函数节流是指规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。节流可以使用在 scroll 函数的事件监听上,通过事件节流来降低事件调用的频率。
function throttle(fn, delay) {
let timer = null;
return (...args) => {
if (timer) return;
timer = setTimeout(() => {
fn(...args);
timer = null;
}, delay);
}
}
3. 实现 AJAX 请求
AJAX是 Asynchronous JavaScript and XML 的缩写,指的是通过 JavaScript 的 异步通信,从服务器获取 XML 文档从中提取数据,再更新当前网页的对应部分,而不用刷新整个网页。
创建AJAX请求的步骤:
- 创建一个 XMLHttpRequest 对象。
- 在这个对象上使用 open 方法创建一个 HTTP 请求,open 方法所需要的参数是请求的方法、请求的地址、是否异步和用户的认证信息。
- 在发起请求前,可以为这个对象添加一些信息和监听函数。比如说可以通过 setRequestHeader 方法来为请求添加头信息。还可以为这个对象添加一个状态监听函数。一个 XMLHttpRequest 对象一共有 5 个状态,当它的状态变化时会触发onreadystatechange 事件,可以通过设置监听函数,来处理请求成功后的结果。当对象的 readyState 变为 4 的时候,代表服务器返回的数据接收完成,这个时候可以通过判断请求的状态,如果状态是 2xx 或者 304 的话则代表返回正常。这个时候就可以通过 response 中的数据来对页面进行更新了。
- 当对象的属性和监听函数设置完成后,最后调用 sent 方法来向服务器发起请求,可以传入参数作为发送的数据体。
function myAJAX(data, method = 'GET', url, isAsync) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open(method, url, isAsync);
// 设置监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error(this.statusText));
}
}
// 设置请求失败时的监听函数
xhr.onerror = function() {
reject(new Error(this.statusText));
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(JSON.stringify(data));
});
}
4. 手写深拷贝函数
function deepCopy(obj) {
if (!obj || typeof obj !== 'object') {
throw new TypeError('the parameter are not object');
}
const newObj = Array.isArray(obj) ? [] : {};
for(let key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}
5. 实现 add(1)(2)(3)
函数柯里化概念: 柯里化(Currying)是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果的新函数的技术。
- 参数长度固定
function add(m) {
var temp = function (n) {
return add(m + n);
}
temp.toString = function () {
return m;
}
return temp;
};
console.log(add(3)(4)(5).toString()); // 12
console.log(add(3)(6)(9)(25).toString()); // 43
对于add(3)(4)(5)
,其执行过程如下:
- 先执行
add(3)
,此时m=3
,并且返回temp
函数; - 执行
temp(4)
,这个函数内执行add(m+n)
,n是此次传进来的数值4,m值还是上一步中的3,所以add(m+n)=add(3+4)=add(7)
,此时m=7
,并且返回temp
函数 - 执行
temp(5)
,这个函数内执行add(m+n)
,n是此次传进来的数值5,m值还是上一步中的7,所以add(m+n)=add(7+5)=add(12)
,此时m=12,并且返回temp
函数 - 由于后面没有传入参数,等于返回的
temp
函数不被执行而是打印,了解JS的朋友都知道对象的toString
是修改对象转换字符串的方法,因此代码中temp
函数的toString
函数return m
值,而m值是最后一步执行函数时的值m=12
,所以返回值是12。
- 参数长度不固定
function add() {
let val = [...arguments].reduce((prev, next) => prev + next);
return function temp() {
if (arguments.length) {
val = [...arguments].reduce((prev, next) => (prev + next), val);
return temp;
} else {
return val;
}
}
}
console.log(add(1, 2)(3, 4, 5)(5)(5, 5)()); // 30
// or
function add() {
let val = [...arguments].reduce((prev, next) => prev + next);
function temp() {
let newVal = [...arguments].reduce((prev, next) => prev + next);
return add(val, newVal);
}
temp.toString = function() {
return val;
}
return temp;
}
console.log(add(1, 2)(3, 4, 5)(5)(5, 5).toString()); // 30
6. 将js对象转化为树形结构
// 转换前:
source = [{
id: 1,
pid: 0,
name: 'body'
}, {
id: 2,
pid: 1,
name: 'title'
}, {
id: 3,
pid: 2,
name: 'div'
}]
// 转换为:
tree = [{
id: 1,
pid: 0,
name: 'body',
children: [{
id: 2,
pid: 1,
name: 'title',
children: [{
id: 3,
pid: 1,
name: 'div'
}]
}
}]
代码实现:
function jsonToTree(data) {
// 初始化结果数组,并判断输入数据的格式
let result = []
if(!Array.isArray(data)) {
return result
}
// 使用map,将当前对象的id与当前对象对应存储起来
let map = {};
data.forEach(item => {
map[item.id] = item;
});
//
data.forEach(item => {
let parent = map[item.pid];
if(parent) {
(parent.children || (parent.children = [])).push(item);
} else {
result.push(item);
}
});
return result;
}
7. 解析 URL Params 为对象
function parseParam(url) {
// 将 ? 后面的字符串取出来
const paramsStr = url.split('?')[1];
// 将字符串以 & 分割后存到数组中
const paramsArr = paramsStr.split('&');
const paramsObj = {};
// 将 params 存到对象中
paramsArr.forEach(param => {
// 处理有 value 的参数
if (param.includes('=')) {
// 分割 key 和 value
let [key, val] = param.split('=');
// 解码
val = decodeURIComponent(val);
// 判断是否转为数字
val = /^\d+$/.test(val) ? parseFloat(val) : val;
// 如果对象有 key,则添加一个值
if (paramsObj.hasOwnProperty(key)) {
paramsObj[key] = [].concat(paramsObj[key], val);
} else { // 如果对象没有这个 key,创建 key 并设置值
paramsObj[key] = val;
}
} else { // 处理没有 value 的参数
paramsObj[param] = true;
}
})
return paramsObj;
}
let url = 'http://www.domain.com/?user=anonymous&id=123&id=456&city=%E5%8C%97%E4%BA%AC&enabled';
console.log(parseParam(url));
/* 结果
{ user: 'anonymous',
id: [ 123, 456 ], // 重复出现的 key 要组装成数组,能被转成数字的就转成数字类型
city: '北京', // 中文需解码
enabled: true, // 未指定值得 key 约定为 true
}
*/
8. 实现发布-订阅模式
class EventCenter {
// 1. 定义事件容器,用来装事件数组
handlers = {}
/**
* 2. 注册事件
* @param {string} type 事件名
* @param {Function} handler 事件处理方法
*/
addEventListener(type, handler) {
// 创建新数组容器
if (!this.handlers[type]) {
this.handlers[type] = [];
}
// 存入事件处理方法
this.handlers[type].push(handler);
}
/**
* 3. 触发事件
* @param {string} type 事件名
* @param {Array} params 方法参数列表
*/
dispatchEvent(type, params) {
// 如果没有注册该事件就抛出异常
if(!this.handlers[type]) {
throw new Error('The event is not registered');
}
// 触发事件
this.handlers[type].forEach(handler => handler(...params));
}
/**
* 4. 移除事件
* @param {string} type 事件名
* @param {Function} handler 事件处理方法
*/
removeEventListener(type, handler) {
// 如果没有注册该事件就抛出异常
if (!this.handlers[type]) {
throw new Error('The event does not exist');
}
if (!handler) {
// 移除该事件的所有处理方法
delete this.handlers[type];
} else {
// 移除事件
this.handlers[type] = this.handlers[type].filter(el => el !== handler);
if (this.handlers[type].length === 0) {
delete this.handlers[type];
}
}
}
}
9. 实现每隔一秒打印 1,2,3,4
- 利用闭包
for (var i = 0; i < 5; i++) {
(function(i) {
setTimeout(function() {
console.log(i);
}, i * 1000);
})(i);
}
- 利用块级作用域
for (let i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, i * 1000);
}
10. 使用 setTimeout 实现 setInterval
在平常开发中,我们很少用到setInterval
。因为在事件循环中,setInterval
的延迟可能会积累,所以setTimeout
比setInterval
要准确。接下来就用setTimeout
来模拟实现setInterval
针对 setInterval
的这个缺点,我们可以使用 setTimeout
递归调用来模拟 setInterval
,这样我们就确保了只有一个事件结束了,我们才会触发下一个定时器事件,这样解决了 setInterval
的问题。
实现思路是使用递归函数,不断地去执行 setTimeout
从而达到 setInterval
的效果
function mySetInterval(fn, timeout) {
const timer = {
flag: true
}
function foo() {
if (timer.flag) {
fn();
setTimeout(foo, timeout)
}
}
setTimeout(foo, timeout);
return timer;
}
11. 手写一个 sleep / delay 函数
sleep
函数既是面试中常问到的一道代码题,也是日常工作,特别是测试中常用的一个工具函数。
const sleep = (seconds) => new Promise(resolve => setTimeout(resolve, seconds))
function delay (func, seconds, ...args) {
return new Promise((resolve, reject) => {
setTimeout(() => {
Promise.resolve(func(...args)).then(resolve)
}, seconds)
})
}
12. 对字符串进行编码压缩
例如:
- 输入:
'aaaabbbccd'
- 输出:
'a4b3c2d1'
,代表 a 连续出现四次,b连续出现三次,c连续出现两次,d连续出现一次
实现
function encode(str) {
if (str.length === 0) return '';
const result = [str[0]];
let index = 1, count = 1;
for(let i = 1; i < str.length; i++) {
if (result[index - 1] === str[i]) {
count ++;
} else {
result[index ++] = '' + count;
count = 1;
result[index ++] = str[i];
}
}
result[index ++] = '' + count;
return result.join('');
}
但是如果只出现一次,不编码数字,如 aaab -> a3b
,那又如何呢?
function encode(str) {
if (str.length === 0) return '';
const result = [str[0]];
let index = 1, count = 1;
for(let i = 1; i < str.length; i++) {
if (result[index - 1] === str[i]) {
count ++;
} else {
if (count > 1) {
result[index ++] = '' + count;
}
count = 1;
result[index ++] = str[i];
}
}
if (count > 1) {
result[index ++] = '' + count;
}
return result.join('');
}
13. 实现 JSONP
JSONP
,全称 JSON with Padding
,为了解决跨域的问题而出现。虽然它只能处理 GET 跨域,虽然现在基本上都使用 CORS 跨域,但仍然要知道它。
JSONP
基于两个原理:
- 动态创建
script
,使用script.src
加载请求跨过跨域 script.src
加载的脚本内容为 JSONP: 即PADDING(JSON)
格式
实现
function jsonp ({ url, onData, params }) {
const script = document.createElement('script')
// 一、为了避免全局污染,使用一个随机函数名
const cbFnName = `JSONP_PADDING_${Math.random().toString().slice(2)}`
// 二、默认 callback 函数为 cbFnName
script.src = `${url}?${stringify({ callback: cbFnName, ...params })}`
// 三、使用 onData 作为 cbFnName 回调函数,接收数据
window[cbFnName] = onData;
document.body.appendChild(script)
}
// 发送 JSONP 请求
jsonp({
url: 'http://localhost:10010',
params: { id: 10000 },
onData (data) {
console.log('Data:', data)
}
})