✨ call、apply、bind
call,apply,bind 都是为了改变函数运行时上下文(this指向)而存在的
- 以上三个函数接收的第一个参数都是 要绑定的this指向.
apply
的第二个参数是一个参数数组,call
和bind
的第二个及之后的参数作为函数实参按顺序传入。bind
不会立即调用,其他两个会立即调用。
手写call
Function.prototype.myOwnCall = function(context, ...args) {
context = context || window;
let fn = Symbol('fn');
context[fn] = this;
const result = thisArg[fn](...args);
delete context[fn];
return result;
}
手写apply
Function.prototype.myOwnApply = function(context, arr) {
context = context || window
let fn = Symbol('fn');
context[fn] = this;
var args = [];
var result = null;
if (!arr) {
result = context[fn]();
} else {
result = context[fn](arr);
}
delete context[fn];
return result;
}
手写bind
把新的 this 绑定到某个函数 func 上,并返回 func 的一个拷贝
let boundFunc = func.bind(thisArg[, arg1[, arg2[, ...argN]]])
- 初级
✅ 使用 ES6 语法实现
❌ 不兼容 IE,不支持 new
// 初级:ES6 新语法 const/...
function bind_1(asThis, ...args) {
const fn = this; // 这里的 this 就是调用 bind 的函数 func
return function (...args2) {
return fn.apply(asThis, ...args, ...args2);
};
}
- 中级
✅ 兼容 IE
❌ 不支持 new
function bind2(asThis) {
var slice = Array.prototype.slice;
var args = slice.call(arguments, 1);
var fn = this;
if (typeof fn !== "function") { // 加入了对调用函数类型的判断
throw new Error("cannot bind non_function");
}
return function () {
var args2 = slice.call(arguments, 0);
return fn.apply(asThis, args.concat(args2));
};
}
- 高级
function bind3(thisArg, ...args) {
const originFunc = this;
const boundFunc = function (...args1) {
// 解决 bind 之后对返回函数 new 的问题
if (new.target) {
if (originFunc.prototype) {
boundFunc.prototype = originFunc.prototype;
}
const res = originFunc.apply(this, args.concat(args1));
return res !== null && (typeof res === 'object' || typeof res === 'function') ? res : this;
} else {
return originFunc.apply(thisArg, args.concat(args1));
}
};
// 解决length 和 name 属性问题
const desc = Object.getOwnPropertyDescriptors(originFunc);
Object.defineProperties(boundFunc, {
length: Object.assign(desc.length, {
value: desc.length < args.length ? 0 : (desc.length - args.length)
}),
name: Object.assign(desc.name, {
value: `bound ${desc.name.value}`
})
});
return boundFunc;
};
✨ 手写深拷贝
简单版
JSON反序列化
const B = JSON.parse(JSON.stringify(A))
❌ JSON value不支持的数据类型,都拷贝不了:
- 不支持函数
- 不支持undefined(支持null)
- 不支持循环引用,比如
a = {name: 'a'}
;a.self = a
;a2 = JSON.parse(JSON.stringify(a))
- 不支持Date,会变成 ISO8601 格式的字符串
- 不支持正则表达式
- 不支持Symbol
复杂版
-
第一版
function isObject(obj){ return Object.prototype.toString.call(obj) === '[Object Object]'; } function clone(source){ if(!isObject(source)) return source; const target = {}; for(let i in target) { if(source.hasOwnProperty(i)){ if(isObject(source[i])){ target[i] = clone(source[i]); } else { target[i] = source[i]; } } } return target; }
- 没有对参数做检验
- 判断是否对象的逻辑不够严谨
- 没有考虑数组的兼容
-
第二版
function isObject(obj){ return typeof obj === 'object' && obj != null; // 兼容数组 } // 使用 循环检测 解决循环引用 function clone(source, hash = new WeakMap()){ if(!isObject(source)) return source; if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表 const target = Array.isArray(source) ? [...source] : {...source}; hash.set(source, target); // 新增代码,哈希表设值 for(let i in target) { if(target.hasOwnProperty(i)){ if(isObject(target[i])){ target[i] = clone(target[i], hash); // 新增代码,传入哈希表 } } } return target; } // 这种方法还可以解决引用丢失问题
没有解决递归爆栈。
-
第三版
将递归改为循环来破解爆栈
function isObject(obj){ return typeof obj === 'object' && obj != null; // 兼容数组 } function clone(source, hash = new WeakMap()){ const root = {}; // 栈 const loopList = [{ parent: root, key: undefined, data: source, }]; while(loopList.length){ // 深度优先 const node = loopList.pop(); const parent = node.parent; const key = node.key; const data = node.data; // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素 let res = parent; if (typeof key !== 'undefined') { res = parent[key] = {}; } // 数据已经存在 if (hash.has(data)) { parent[key] = hash.get(data); continue; // 中断本次循环 } // 数据不存在 // 保存源数据,在拷贝数据中对应的引用 hash.set(data, res); for(let i in data) { if(data.hasOwnProperty(i)){ if(isObject(data[i])){ // 下一次循环 loopList.push({ parent: res, key: i, data: data[i], }); } else { res[k] = data[k]; } } } } return root; }
✨ 手写防抖&节流
- 防抖
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
- 节流
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放/滚动场景:监控浏览器resize,监控scroll高度
- 动画场景:避免短时间内多次触发动画引起性能问题
// 防抖函数 debounce
const debounce = (fn, delay = 500) => {
let timer = null;
return (...args) => {
if (timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn.apply(this, args);
}, delay)
}
}
// 进阶版 debounce
// leading 表示进入时是否立即执行
const debounce = function(fn, delay = 500, options: {leading: true, context: null}){
let timer = null;
let res;
const _debounce = function (...args) {
options.context || (options.context = this);
if(timer) clearTimeout(timer);
if(options.leading && !timer){
timer = setTimeout(()=>{timer = null}, delay);
res = fn.apply(option.context, args);
} else {
timer = setTimeout(() => {
res = fn.apply(option.context, args);
timer = null;
}, delay);
}
return res;
}
_debounce.cancle = function(){
clearTimeout(timer);
timer = null;
}
return _debounce;
}
// 节流函数 throttle
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
// 上一次执行 fn 的时间
let previous = 0
// 将 throttle 处理结果当作函数返回
return function(...args) {
// 获取当前时间,转换成时间戳,单位毫秒
let now = +new Date()
// 将当前时间和上一次执行函数的时间进行对比
// 大于等待时间就把 previous 设置为当前时间并执行函数 fn
if (now - previous > wait) {
previous = now
fn.apply(this, args)
}
}
}
// 进阶版 throttle
// leading 表示进入时是否立即执行,trailing 表示是否在最后额外触发一次
const throttle = (fn, wait = 50, options: {leading: true, trailing: false, context: null}){
let previous = 0;
let res = ;
let timer;
const _throttle = function(...args){
options.context || (options.context = this);
let now = Date.now();
if(!previous && !options.leading) previous = now;
if(now - previous >= wait){
if (timer) {
clearTimeout(timer);
timer = null;
}
res = fn.apply(options.context, args);
previous = now;
} else if(!timer && options.trailing){
timer = setTimeout(()=>{
res = fn.apply(options.context, args);
previous = 0;
timer = null;
}, wait);
}
return res;
}
_throttle.cancel = function () {
previous = 0;
clearTimeout(timer);
timer = null;
};
return _throttle;
}
// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log('fn 函数执行了'), 1000)
// 每 10 毫秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10)
实现instanceOf
function instance_of(L, R) {
let O = R.prototype;
L = L.__proto__; // 取 L 的隐式原型,等同于 L = Object.getPrototypeOf(L);
while(L){
if(L === O) return true;
L = L.__proto__;
}
return false;
}
// 等同于
function instance_of (L, R) {
return right.prototype.isPrototypeOf(L);
};
实现new
new 执行过程如下:
- 创建一个新对象;
- 新对象的[[prototype]]特性指向构造函数的prototype属性;
- 构造函数内部的this指向新对象;
- 执行构造函数;
- 如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象;
const myOwnNew = (constructor, ...args) => {
let target = {}
target.__proto__ = constructor.prototype; // 等同于 target = Object.create(constructor.prototype);
const res = constructor.apply(target, args)
return res instanceof Object ? res : target;
}
const a = function(){
return 'a';
}
const a1 = myOwnNew(a);
实现Object.create
function create(obj){
function f(){};
f.proptotype = obj;
return new f();
}
实现Object.is
Object.is() 和 === 的区别是 Object.is(0, -0)
返回 false, Object.is(NaN, NaN)
返回 true。
// polyfill
const iIs = function (x, y) {
if (x === y) {
return x !== 0 || 1 / x === 1 / y; // 0, -0
} else {
return x !== x && y !== y; // NaN
}
}
实现flat
// reduce + 递归
function flat(arr, num = 1) {
return num > 0
? arr.reduce(
(pre, cur) =>
pre.concat(Array.isArray(cur) ? flat(cur, num - 1) : cur),
[]
)
: arr.slice();
}
const arr = [1, 2, 3, 4, [1, 2, 3, [1, 2, 3, [1, 2, 3]]], 5, "string", { name: "弹铁蛋同学" }]
flat(arr, Infinity);
// [1, 2, 3, 4, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, "string", { name: "弹铁蛋同学" }];
函数柯里化
将一个多参数函数转化为多个嵌套的单参数函数。
const curry = function (targetFn) {
return function fn (...rest) {
if (targetFn.length === rest.length) {
return targetFn.apply(null, rest);
} else {
return fn.bind(null, ...rest);
}
};
};
// 用法
function add (a, b, c, d) {
return a + b + c + d;
}
console.log('柯里化:', curry(add)(1)(2)(3)(4));
// 柯里化:10
实现CO(协程)
const co = (gen) =>{
return new Promise((resolve, reject) => {
if(typeof gen === 'function' ) gen = gen();
if(!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(v){
let res;
try {
res = gen.next(v);
} catch (error) {
reject(error);
}
next(res);
}
function onRejected(err){
let res;
try {
res = gen.throw(err);
} catch (e) {
return reject(e);
}
next(res);
}
function next(res){
if(res.done) return resolve(res.value);
let nextGen = Promise.resolve(res.value);
return nextGen.then(onFulfilled, onRejected);
}
});
}
// 测试
const gen = function *() {
console.log('start');
let res1 = yield Promise.resolve(1);
console.log(res1);
let res2 = yield Promise.resolve(2);
console.log(res2);
let res3 = yield Promise.resolve(3);
console.log(res3);
return res1 + res2 + res3;
}
co(gen).then(value => {
console.log('add: ' + value);
}, function (err) {
console.error(err.stack);
});
原生XHR请求
XMLHttpRequest (XHR) 是一种创建 AJAX 请求的 JavaScript API 。
function reqListener () {
console.log(this.responseText);
}
var xhr = new XMLHttpRequest();
xhr.addEventListener("load", reqListener); // 请求成功完成时触发
xhr.open("GET", "http://www.example.org/example.txt"); // 初始化一个请求 (method, url, async, user, password)
xhr.onloadstart() // 在XMLHttpRequest**开始传送数据**时被调用
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(xhr.responseText);
}
}
// 只要 readyState 属性发生变化,就会调用相应的处理函数。
/**
0 UNSENT 代理被创建,但尚未调用 open() 方法。
1 OPENED open() 方法已经被调用。
2 HEADERS_RECEIVED send() 方法已经被调用,并且头部和状态已经可获得。
3 LOADING 下载中 responseText 属性已经包含部分数据。
4 DONE 下载操作已完成。
*/
xhr.send(); // 发送请求 (body)
实现Promise版JSONP
function jsonp({url, params, callback}) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
window[callback] = function(data){
resolve(data);
document.body.removeChild(script);
}
params = {...params, callback};
let arrs = [];
for(key in params){
arrs.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arrs.join('&')}`;
document.body.appendChild(script);
});
}