call函数 1 2 3 4 5 6 7 8 9 10 11 12 Function.prototype.selfCall = function(context) { const ctx = context || window; // 去除第一个参数 const arg = [...arguments].slice(1); // const arg = Array.slice.call(arguments, 1); // 将函数赋值给ctx.fnc ctx.fnc = this; // 执行函数 const res = ctx.fnc(...arg); Reflect.deleteProperty(ctx, "fnc") return res }
上面的方法借用了很多ES6的方法,让我们看一下比较原始的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Function.prototype.call2 = function (context) { var context = context || window; context.fn = this; var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context.fn(' + args +')'); delete context.fn return result; }
apply函数 1 2 3 4 5 6 7 8 9 Function.prototype.selfApply = function (context) { const ctx = context || window; ctx.fnc = this; const arg = [...arguments].slice(1)[0]; // const arg = Array.slice.call(arguments, 1)[0]; const res = ctx.fnc(...arg) Reflect.deleteProperty(ctx, "fnc") return res }
不借用ES6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 Function.prototype.apply = function (context, arr) { var context = Object(context) || window; context.fn = this; var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') } delete context.fn return result; }
关于 Object(context) 原文是这么说的:
非严格模式下,指定为 null 或 undefined 时会自动指向全局对象,郑航写的是严格模式下的,我写的是非严格模式下的,实际上现在的模拟代码有一点没有覆盖,就是当值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
bind函数
首先书写了一个简单的bind函数,并不包括 new 操作
1 2 3 4 5 6 7 Function.prototype.myBind = function (ctx) { const args = [...arguments].slice(1) const self = this return function () { return self.apply(ctx, [...args, ...arguments]) } }
构造函数效果的模拟实现 // TODO
new 操作符 首先分析一下 new 操作符
1 2 3 4 5 6 7 8 function Student(name, age) { this.name = name this.age = age } Student.prototype.say = function () { console.log(this.name); } var jojo = new Student("jsd", 23);
从结果分析
返回了一个对象,其实例属性是通过构造函数(Student)生成的
对象的__proto__
指向Student.prototype
创建一个空对象obj({})
将obj的[[prototype]]
属性指向构造函数constrc的原型(即obj.[[prototype]]
= constrc.prototype)。
将构造函数constrc内部的this绑定到新建的对象obj,执行constrc(也就是跟调用普通函数一样,只是此时函数的this为新创建的对象obj而已,就好像执行obj.constrc()一样);
若构造函数没有返回非原始值(即不是引用类型的值),则返回该新建的对象obj(默认会添加return this)。否则,返回引用类型的值。
官方解释
创建一个空的简单JavaScript对象(即{});
链接该对象(设置该对象的constructor)到另一个对象 ;
将步骤1新创建的对象作为this的上下文 ;
如果该函数没有返回对象,则返回this。 实现方式如下:
1 2 3 4 5 6 function createClass(_Class, ...rest) { const obj = {} const res = _Class.apply(obj, rest) obj.__proto__ = _Class.prototype return typeof res === "object" ? res : obj }
利用Object.create稍微简化一下
1 2 3 4 5 6 7 function newFactorSimple(ctor) { const arg = [...arguments].slice(1); // 生成一个__proto__指向ctor.prototype的对象 var obj = Object.create(ctor.prototype); ctor.call(obj, ...arg); return obj; }
其实构造函数内含有return语句时,结果会出现差异
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 // 例子4 function Student(name){ this.name = name; // Null(空) null // Undefined(未定义) undefined // Number(数字) 1 // String(字符串)'1' // Boolean(布尔) true // Symbol(符号)(第六版新增) symbol // Object(对象) {} // Function(函数) function(){} // Array(数组) [] // Date(日期) new Date() // RegExp(正则表达式)/a/ // Error (错误) new Error() // return /a/; } var student = new Student('若川'); console.log(student); {name: '若川'}
前面六种基本类型都会正常返回{name: ‘若川’},后面的Object(包含Functoin, Array, Date, RegExg, Error)都会直接返回这些值下面是考虑到各种情况后的new实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 模拟实现 new 操作符 * @param {Function} ctor [构造函数] * @return {Object|Function|Regex|Date|Error} [返回结果] */ function newOperator(ctor){ if(typeof ctor !== 'function'){ throw 'newOperator function the first param must be a function'; } // ES6 new.target 是指向构造函数 newOperator.target = ctor; var newObj = Object.create(ctor.prototype); // ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments); // 除去ctor构造函数的其余参数 var argsArr = [].slice.call(arguments, 1); // 3.生成的新对象会绑定到函数调用的`this`。 // 获取到ctor函数返回结果 var ctorReturnResult = ctor.apply(newObj, argsArr); // 小结4 中这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null,排除null var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null; var isFunction = typeof ctorReturnResult === 'function'; if(isObject || isFunction){ return ctorReturnResult; } // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。 return newObj; }
create函数 很多框架源码作者使用它来初始化一个新的对象,难道是最佳实践? 原因有二
通过Object.create(null)创建出来的对象,没有任何属性,显示No properties。我们可以将其当成一个干净的 map 来使用,自主定义 toString,hasOwnProperty等方法,并且不必担心将原型链上的同名方法被覆盖。
{…}创建的对象,使用for in遍历对象的时候,会遍历原型链上的属性,带来性能上的损耗。使用Object.create(null)则不必再对其进行遍历了。
手写Object.create
1 2 3 4 5 6 7 8 9 10 function ObjCreate(proto, properties) { // 判断类型,第一个参数传入的必须是 object, function if (typeof proto !== "object" && typeof proto !== "function") { throw new TypeError("Object prototype may only be an Object: " + proto); } // 简单的实现的过程,忽略了properties var func = function() {}; func.prototype = proto; // 将fn的原型指向传入的proto return new func(); // 返回创建的新对象,这里思考下,new func() 又做了什么事情呢?且往下看! };
new func()的作用是创建一个新的对象,其中func是一个构造函数,在这个过程中,主要包含了如下步骤:
创建空对象obj;
将obj的原型设置为构造函数的原型,obj.proto = func.prototype;
以obj为上下文执行构造函数,func.call(obj);
返回obj对象。
手写promise 所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
摘自阮一峰ES6深入理解
引入三个问题 这三个问题可以帮助理解
1、Promise 中为什么要引入微任务?
由于promise采用.then延时绑定回调机制,而new Promise时又需要直接执行promise中的方法,即发生了先执行方法后添加回调的过程,此时需等待then方法绑定两个回调后才能继续执行方法回调,便可将回调添加到当前js调用栈中执行结束后的任务队列中,由于宏任务较多容易堵塞,则采用了微任务
2、Promise 中是如何实现回调函数返回值穿透的?
首先Promise的执行结果保存在promise的data变量中,然后是.then方法返回值为使用resolved或rejected回调方法新建的一个promise对象,即例如成功则返回new Promise(resolved),将前一个promise的data值赋给新建的promise
3、Promise 出错后,是怎么通过“冒泡”传递给最后那个捕获
promise内部有resolved_和rejected_变量保存成功和失败的回调,进入.then(resolved,rejected)时会判断rejected参数是否为函数,若是函数,错误时使用rejected处理错误;若不是,则错误时直接throw错误,一直传递到最后的捕获,若最后没有被捕获,则会报错。可通过监听unhandledrejection事件捕获未处理的promise错误
简单的promise 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 function MyPromise(executor) { var _this = this; this._status = "pending"; this._successCallBack = null; this._errorCallBack = null; var resolve = function (res) { if (_this._status === "pending") { _this._status = "fulfilled"; // 运行then函数传递过来的成功函数 // 并将结果作为参数回传 _this._successCallBack(res); } } var reject = function (res) { if (_this._status === "pending") { _this._status = "rejected"; // 运行then函数传递过来的错误函数 // 并将结果作为参数回传 _this._errorCallBack(res); } } // 把内部函数resolve, reject作为参数,把传进来的函数执行一遍 setTimeout(() => { // 使用setTimeout 是让resolve晚于then方法赋值 executor(resolve, reject) }, 0) } MyPromise.prototype.then = function (sucess, error) { this._successCallBack = sucess; this._errorCallBack = error; }
进阶的promise 实现then的链式调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 class MyPromise { PromiseResult = null; // 终值 PromiseState = "pending"; // 状态 toDoFulFilled = null; toDoRejected = null; // 构造方法 constructor(executor) { const resolve = (value) => { setTimeout(() => { console.log("resolve"); if (this.PromiseState !== "pending") return; this.PromiseState = "fulfilled"; this.PromiseResult = value; if (this.toDoFulFilled) { this.toDoFulFilled(this.PromiseResult); } }, 0); }; const reject = (reason) => { setTimeout(() => { if (this.PromiseState !== "pending") return; this.PromiseState = "rejected"; this.PromiseResult = reason; if (this.toDoRejected) { this.toDoRejected(this.PromiseResult); } }, 0); }; // 执行传进来的函数 try { executor(resolve, reject); } catch (error) { reject(error); } } then(onFulfilled, onRejected) { return new MyPromise((resolve, reject) => { const resolvePromise = (cb) => { try { // 这个prevRes 就是本次then的返回值 // cb 就是 onFulfilled onRejected const prevRes = cb(this.PromiseResult); // TODO if (prevRes instanceof MyPromise) { prevRes.then(resolve, reject); } else { resolve(prevRes); } } catch (err) { reject(err); throw new Error(err); } }; this.toDoFulFilled = resolvePromise.bind(this, onFulfilled); this.toDoRejected = resolvePromise.bind(this, onRejected); }); } } /* const test1 = new MyPromise((resolve, reject) => { resolve("成功"); }).then( (res) => { console.log(res); }, (error) => { console.log(error); } ); */ const test2 = new MyPromise((resolve, reject) => { setTimeout(() => { resolve("定时器"); }, 2000); }) .then( (res) => { console.log(res); return res + "链式调用"; }, (error) => { console.log(error); } ) .then( (res) => { console.log(res); }, (error) => { console.log(error); } );
限制promise并发 主要的点
在then方法里启动下一次任务
while为了塞满队列
task是一个待启动的promise
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 class MaxDuty{ used = 0 tasks = [] constructor(max) { this.max = max } addTask(task) { this.tasks.push(task) } walk() { if (this.tasks.length && this.used < this.max) { this.used++ const task = this.tasks.shift() task().then(res => { console.log(res); this.used-- this.walk() }) } } start() { // 需要塞满 while (this.used < this.max) { this.walk() } } } // 返回一个待启动的promise function createTask(time, order) { return () => { return new Promise(resolve => { setTimeout(() => { resolve(order) }, time); }) } } const maxDuty = new MaxDuty(2) maxDuty.addTask(createTask(1000, 1)) maxDuty.addTask(createTask(500, 2)) maxDuty.addTask(createTask(300, 3)) maxDuty.addTask(createTask(400, 4)) maxDuty.start()
珂里化 第一版 比较简单,也好理解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var curry = function (fn) { // 取到fn之后的参数 var args = [].slice.call(arguments, 1); // 返回一个待使用的新函数 return function () { // 将新函数的参数 和 之前生成珂里化函数的参数合并 var newArgs = args.concat([].slice.call(arguments)); // 不改变this,使用所有参数进行调用 return fn.apply(this, newArgs); }; }; function add(a, b) { return a + b; } var addCurry = curry(add, 1, 2); addCurry() // 3 //或者 var addCurry = curry(add, 1); addCurry(2) // 3 //或者 var addCurry = curry(add); addCurry(1, 2) // 3
固定参数 第二版 稍微复杂一些,先看实现的效果会比较好理解一点
1 2 3 4 5 6 7 8 var fn = curry(function (a, b, c) { console.log([a, b, c]); }); fn("a", "b", "c"); // ["a", "b", "c"] fn("a", "b")("c"); // ["a", "b", "c"] fn("a")("b")("c"); // ["a", "b", "c"] fn("a")("b", "c"); // ["a", "b", "c"]
首先需要明确1点
来看实现过程
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function curry (fn ) { const length = fn.length let args = [].slice .call (arguments , 1 ) return function ( ) { const argChild = [...args,...arguments ] if (argChild.length === length) { return fn.apply (this , argChild) } else { return curry.call (this , fn, ...argChild) } } } var fn1 = curry (function (a, b, c ) { console .log ([a, b, c]); }); fn1 ("a" , "b" , "c" ) fn1 ("a" , "b" )("c" ) fn1 ("a" )("b" )("c" ) fn1 ("a" )("b" , "c" ) var fn2 = curry (function (a, b, c ) { console .log ([a, b, c]); }, "a" ); fn2 ("b" , "c" ) fn2 ("b" )("c" )
不固定参数 这种方法太刻意了,建议闲下来的时候看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 // fn(a,b,c) = fn(a)(b)(c) function curry(fn) { return function core(...args) { let params = [] params = params.concat(args) let inner = function(...args2) { params = params.concat(args2) return core.apply(this, params) } inner.toString = ()=>{ return fn.apply(null, params) } return inner } } function add(...args) { return args.reduce((prev,curr)=>prev + curr, 0) } let curriedAdd = curry(add) alert(curriedAdd(1,2,3)) alert(curriedAdd(1, 2)(1, 2, 3)) curriedAdd(1)(2)(3)(4)(5)
防抖 参考
有一些需要注意的点,可以帮助理解函数,重点看代码中注释的地方
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> html, body { height: 100%; width: 100%; } </style> </head> <body> <script> var count = 1 function handle(event) { console.log(this); } document.onmousemove = debounce(handle, 400) function debounce(fn, wait) { var timer = null return function () { clearTimeout(timer) timer = setTimeout( // 这么写 实际上没有效果 fn.apply(this, arguments), // 下面的写法均有效果 fn.bind(this, arguments), fn, wait ) } } </script> </body> </html>
需要注意的是,setTimeout第一个参数需要是一个函数,而fn.apply(this, arguments)
不能算一个函数
下面写一个比较标准的debounce,可以正确响应参数和this
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function debounce(fn, wait) { var timer = null return function () { clearTimeout(timer) timer = setTimeout( fn.bind(this, ...arguments), wait ) } } // 不使用es6 function debounce(func, wait) { var timeout; return function () { var context = this; var args = arguments; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context, args) }, wait); } }
立刻执行 这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。
这个需求就是:
我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行 。
其实这个实现也算简单,主要是理解需求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function immedia(fn, wait) { var flag = true var timer = null return function () { clearTimeout(timer) // 保证不在触发后wait时间后,可以再次被触发 timer = setTimeout( function () { flag = true }, wait ) if(flag){ // 触发后 开启保护 flag = false fn.apply(this, arguments) } } }
写在一个函数里,也很简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 function debounce(fn, wait, immediate) { var timer = null if(immediate){ var flag = true return function () { clearTimeout(timer) timer = setTimeout( function () { flag = true }, wait ) if(flag){ flag = false fn.apply(this, arguments) } } }else{ return function () { clearTimeout(timer) timer = setTimeout( fn.bind(this, ...arguments), wait ) } } }
节流 节流的原理很简单:
如果你持续触发事件,每隔一段时间,只执行一次事件。
关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
时间戳 1 2 3 4 5 6 7 8 9 10 function throttle(fn, wait) { const previous = 0; return function () { const now = new Date().getTime() if (now - previous > wait) { previous = now } fn.apply(this, arguments) } }
定时器 1 2 3 4 5 6 7 8 9 10 11 12 function throttle(fn, wait) { let flag = true return function () { if (flag) { flag = false fn.apply(this, arguments) setTimeouto=(function () { flag = true },wait) } } }
deepclone 先看一个足够用的版本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function deepClone(obj) { if (obj === 'null') return null if (typeof obj !== 'object') return obj if (obj instanceof RegExp) return new RegExp(obj) if (obj instanceof Date) return new Date(obj) let result = new obj.constructor for (let key in obj) { if (obj.hasOwnProperty(key)) { result[key] = deepClone(obj[key]) } } // 上面的for循环可以改成 // Reflect.ownKeys(key => result[key] = deepClone(obj[key]) return result }
首先实现一个简单clone,包括对象 数组和基本类型的clone
1 2 3 4 5 6 7 8 9 10 11 function deepClone(target) { if (typeof target !== "object") { return target } else { const copy = Array.isArray(target) ? [] : {} for (const key in target) { copy[key] = deepClone(target[key]) } return copy } }
如果出现循环引用,就会栈溢出,如何解决这个问题呢,需要借助map数据结构
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储key-value
形式的数据,且key
可以是一个引用类型,我们可以选择Map
这种数据结构:
检查map
中有无克隆过的对象
有 - 直接返回
没有 - 将当前对象作为key
,克隆对象作为value
进行存储
继续克隆
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 function deepClone(target) { const map = new Map(); return (function _clone(target) { if (typeof target !== "object") { return target } else { if (map.has(target)) { return target } map.set(target, null) const copy = Array.isArray(target) ? [] : {} for (const key in target) { copy[key] = _clone(target[key]) } return copy } })(target) } const target = { field1: 1, field2: undefined, field3: { child: 'child' }, field4: [2, 4, 8] }; target.target = target console.log(deepClone(target));
控制台结果如下
1 2 3 4 5 6 7 8 9 10 11 12 13 { field1: 1, field2: undefined, field3: { child: 'child' }, field4: [ 2, 4, 8 ], target: <ref *1> { field1: 1, field2: undefined, field3: { child: 'child' }, field4: [ 2, 4, 8 ], target: [Circular *1] } }
circular
表示循环引用的意思,把Map换成weakMap,Map有强引用,不利用垃圾收集