盘点那些 JS 手写题
1. JS 基础
1. 手写 Object.create
❝❞
Object.create()
方法创建一个新对象,使用现有的对象来提供新创建的对象的__ proto __
「语法」
// 返回一个新对象,带着指定的原型对象和属性。 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 方法
❝❞
instanceof
运算符用于判断构造函数的prototype
属性是否出现在对象的原型链中的任何位置。
「语法」
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
的完成顺序决定。
「实现」
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)状态。
「实现」
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) } })