手写源码系列 数组去重 使用双重 for
和 splice
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const unique = arr => { for (let i=0 ;i<arr.length;i++){ for (let j= i+1 ;j<arr.length;j++){ if (arr[i]===arr[j]){ arr.splice(j,1 ); j--; } } } return arr; } a=[1 ,5 ,4 ,6 ,8 ,2 ,1 ,5 ]; console .log(a);unique(a); console .log(a);
使用 indexOf
或 includes
加新数组 1 2 3 4 5 6 7 8 9 10 11 12 const unique = arr => { const uniqueArr = []; for (let i=0 ;i<arr.length;i++){ if (uniqueArr.indexOf(arr[i])=== -1 ){ uniqueArr.push(arr[i]); } } return uniqueArr; }
sort
排序后,使用快慢指针的思想1 2 3 4 5 6 7 8 9 10 11 12 13 const unique = arr => { arr.sort((a,b )=> a-b); let slow = 1 ,fast =1 ; while (fast<arr.length){ if (arr[fast] !== arr[fast-1 ]){ arr[slow++] = arr[fast]; } fast++; } arr.length = slow; return arr; }
sort
方法用于从小到大排序(返回一个新数组),其参数中不带以上回调函数就会在两位数及以上时出现排序错误(如果省略,元素按照转换为的字符串的各个字符的 Unicode
位点进行排序。两位数会变为长度为二的字符串来计算)。
ES6
提供的 Set
去重1 2 3 4 5 6 7 8 9 const unique = arr => { const res = new Set (arr); return [...res]; }
Set
中的元素只会出现一次,即 Set
中的元素是唯一的。
使用哈希表存储元素是否出现(ES6
提供的 map
) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 const unique = arr => { const map = new Map (); const uniqueArr= new Array (); for (let i =0 ;i<arr.length;i++){ if (!map.has(arr[i])){ map.set(arr[i],true ); uniqueArr.push(arr[i]); } } return uniqueArr; }
map
对象保存键值对,与对象类似。但 map
的键可以是任意类型,对象的键只能是字符串类型。
如果数组中只有数字也可以使用普通对象作为哈希表。
filter
配合 indexOf
filter返回过滤后的数组。filter也接收一个函数作为参数,这个函数将作用于数组中的每个元素,根据该函数每次执行后返回的布尔值来保留结果,如果是true就保留,如果是false就过滤掉。
1 2 3 const unique = arr => { return arr.filter((item,index,arr ) => arr.indexOf(item)===index); }
这里有可能存在疑问,我来举个例子:
1 2 3 const arr = [1 ,1 ,2 ,1 ,3 ]arr.indexOf(arr[0 ]) === 0 arr.indexOf(arr[1 ]) !== 1
reduce
配合 includes
reduce也是返回一个全新的数组。reduce接受一个函数作为参数,这个函数要有两个形参,代表数组中的前两项,reduce会将这个函数的结果与数组中的第三项再次组成这个函数的两个形参以此类推进行累积操作。
1 2 3 4 5 6 7 const unique = arr => { const uniqueArr = arr.reduce((res,cur ) => { if (!res.includes(cur))res.push(cur); return res; },[]) return uniqueArr; }
如何实现数组扁平化 数组扁平化就是把多维数组转化成一维数组。
ES6提供的新方法 flat(depth) 不会改变原数组
1 2 3 let a = [1 ,[2 ,3 ,[4 ,[5 ]]]]; a.flat(4 -1 ); a.flat(Infinity );
reduce方法 递归 这里使用的是数组的reduce方法,需要注意的是reduce方法,我们传递了两个参数, 第一个参数就是就是处理扁平化的箭头函数 第二个参数是一个空数组,也是作为遍历的开始。(res)
1 2 3 4 5 6 const flatten = arr => { return arr.reduce((res,next )=> { return res.concat(Array .isArray(next)?flatten(next):next); },[]) }
模拟实现 new 操作符 new 运算符 创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作:
创建一个空的简单JavaScript对象(即{}
); 将该对象的__proto__
指向构造函数原型; 将步骤1新创建的对象作为this
的上下文 ; 如果该函数没有返回对象,则返回this
。 1 2 3 4 5 6 7 8 9 10 11 12 function new_object ( ) { let obj = new Object () let Con = [].shift.call(arguments ) obj.__proto__ = Con.prototype let result = Con.apply(obj, arguments ) return typeof result === 'object' ? result : obj }
警告: 通过现代浏览器的操作属性的便利性,可以改变一个对象的 [[Prototype]]
属性, 这种行为在每一个JavaScript引擎和浏览器中都是一个非常慢且影响性能的操作,使用这种方式来改变和继承属性是对性能影响非常严重的,并且性能消耗的时间也不是简单的花费在 obj.__proto__ = ...
语句上, 它还会影响到所有继承来自该 [[Prototype]]
的对象,如果你关心性能,你就不应该在一个对象中修改它的 [[Prototype]]。相反, 创建一个新的且可以继承 [[Prototype]]
的对象,推荐使用 Object.create()
—MDN
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function create ( ) { Con = [].shift.call(arguments ); let obj = Object .create(Con.prototype); let ret = Con.apply(obj, arguments ); return ret instanceof Object ? ret : obj; }; function Car (color ) { this .color = color; } Car.prototype.start = function ( ) { console .log(this .color + " car start" ); } let car = create(Car,"black" );
模拟实现 instanceOf instanceof
判断对象的原型链上是否存在构造函数的原型。只能判断引用类型。instanceof
常用来判断 A
是否为 B
的实例1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const instance_of = (Case,Constructor ) => { if ((typeof (Case) !== 'object' && typeof (Case) !== 'function' ) || Case === null ){ return false ; } let CaseProto = Case.__proto__; while (true ){ if (CaseProto === null ) return false ; if (CaseProto === Constructor.prototype) return true ; CaseProto = Case.__proto__; } } let a={};console .log(instance_of(a,Object ))
浅拷贝和深拷贝? 重要: 什么是拷贝? 首先来直观的感受一下什么是拷贝。
1 2 3 4 5 let arr = [1 , 2 , 3 ];let newArr = arr;newArr[0 ] = 100 ; console .log(arr);
这是直接赋值的情况,不涉及任何拷贝。当改变newArr的时候,由于是同一个引用,arr指向的值也跟着改变。
现在进行浅拷贝:
1 2 3 4 5 let arr = [1 , 2 , 3 ];let newArr = arr.slice();newArr[0 ] = 100 ; console .log(arr);
当修改newArr的时候,arr的值并不改变。什么原因?因为这里newArr是arr浅拷贝后的结果,newArr和arr现在引用的已经不是同一块空间啦!
这就是浅拷贝!
但是这又会带来一个潜在的问题:
1 2 3 4 5 let arr = [1 , 2 , {val : 4 }];let newArr = arr.slice();newArr[2 ].val = 1000 ; console .log(arr);
咦!不是已经不是同一块空间的引用了吗?为什么改变了newArr改变了第二个元素的val值,arr也跟着变了。
这就是浅拷贝的限制所在了。它只能拷贝一层对象。如果有对象的嵌套,那么浅拷贝将无能为力。但幸运的是,深拷贝就是为了解决这个问题而生的,它能 解决无限极的对象嵌套问题,实现彻底的拷贝。当然,这是我们下一篇的重点。 现在先让大家有一个基本的概念。
接下来,我们来研究一下JS中实现浅拷贝到底有多少种方式?
浅拷贝 1. 手动实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 const shallowClone = source => { if (typeof source === 'object' && source !== null ){ const output = Array .isArray(source) ? [] : {} ; for (let key in source){ if (source.hasOwnProperty(key)){ output[key] = source[key]; } } return output; }else { return source; } }
2. Object.assign Object.assign()
方法用于将所有可枚举属性的值从一个或多个源对象分配到目标对象。它将返回目标对象。
但是需要注意的是,Object.assgin() 拷贝的是对象的属性的引用,而不是对象本身。
1 2 3 let obj = { name : 'sy' , age : 18 };const obj2 = Object .assign({}, obj, {name : 'sss' });console .log(obj2);
3. concat浅拷贝数组 concat()
方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
1 2 3 4 let arr = [1 , 2 , 3 ];let newArr = arr.concat();newArr[1 ] = 100 ; console .log(arr);
4. slice浅拷贝 slice()
方法返回一个新的数组对象,这一对象是一个由 begin
和 end
决定的原数组的浅拷贝 (包括 begin
,不包括end
)。原始数组不会被改变。
5. …展开运算符 1 2 let arr = [1 , 2 , 3 ];let newArr = [...arr];
深拷贝 第一种:JSON.parse(JSON.stringify(object))
估计这个api能覆盖大多数的应用场景,没错,谈到深拷贝,我第一个想到的也是它。但是实际上,对于某些严格的场景来说,这个方法是有巨大的坑的。问题如下:
无法解决循环引用
的问题。举个例子: 1 2 const a = {val :2 };a.target = a;
拷贝a会出现系统栈溢出,因为出现了无限递归
的情况。
无法拷贝一写特殊的对象
,诸如 RegExp, Date, Set, Map等。
无法拷贝函数
(划重点)。
会忽略 undefined
会忽略 symbol
就目前而言,第三方最完善的深拷贝方法是 Lodash 库的 _.cloneDeep()
方法了。在实际项目中,如需处理 JSON.stringify()
无法解决的 Case,我会推荐使用它 。
面试够用版
主要运用到递归的思路去实现一个深拷贝方法。
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 const deepClone = source => { const isArray = arr => Object .prototype.toString.call(arr) === '[object Array]' ; const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function' ); const copy = input => { if (typeof input === 'function' || !isObject(input)) return input; const output = isArray(input) ? [] : {} ; for (let key in input){ if (input.hasOwnProperty(key)){ output[key] = copy(input[key]); } } return output; } return copy(source); } let arr = [1 , 2 , {val : 4 }];let newArr1 = deepClone(arr);newArr1[2 ].val = 1000 ; let newArr2 = newArr1.slice();newArr2[2 ].val = 2000 ; console .log(arr);console .log(newArr1);console .log(newArr2)
需要进一步改进的问题
主要有循环引用 、包装对象、函数、原型链、不可枚举属性、Map/WeakMap、Set/WeakSet、RegExp、Symbol、Date、ArrayBuffer、原生 DOM/BOM 对象等。
针对循环引用的问题 以下是一个循环引用(circular reference)的对象:
1 2 const foo = { name : 'Frankie' }foo.bar = foo
使用WeakMap 解决
首先,Map 的键属于强引用,而 WeakMap 的键则属于弱引用。且 WeakMap 的键必须是对象,WeakMap 的值则是任意的。
由于它们的键与值的引用关系,决定了 Map 不能确保其引用的对象不会被垃圾回收器回收的引用。假设我们使用的 Map,那么图中的 foo
对象和我们深拷贝内部的 const map = new Map()
创建的 map
对象一直都是强引用关系,那么在程序结束之前,foo
不会被回收,其占用的内存空间一直不会被释放。
相比之下,原生的 WeakMap 持有的是每个键对象的“弱引用”,这意味着在没有其他引用存在时垃圾回收能正确进行。原生 WeakMap 的结构是特殊且有效的,其用于映射的 key 只有在其没有被回收时才是有效的。
基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 const deepClone = source => { const isArray = arr => Object .prototype.toString.call(arr) === '[object Array]' ; const isObject = obj => obj !== null && (typeof obj === 'object' || typeof obj === 'function' ); const copy = input => { if (typeof input === 'function' || !isObject(input)) return input; const output = isArray(input) ? [] : {} ; for (let key in input){ if (input.hasOwnProperty(key)){ output[key] = copy(input[key]); } } return output; } return copy(source); }
这里提供另一个思路,也是可以的。
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 const deepCopy = source => { const copiedArr = [] const copy = input => { if (typeof input === 'function' || !isObject(input)) return input; for (let i = 0 , len = copiedArr.length; i < len; i++) { if (input === copiedArr[i].key) return copiedArr[i].value } const output = isArray(input) ? [] : {} copiedArr.push({ key : input, value : output }) } return copy(source) }
解析 call/apply 原理,并手写 call/apply 实现 Function.prototype.call() call()
方法调用一个函数, 其具有一个指定的 this
值和多个参数(参数的列表 )。
1 func.call(thisArg, arg1, arg2, ...)
它运行 func
,提供的第一个参数 thisArg
作为 this
,后面的作为参数。
看一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 function sayWord ( ) { var talk = [this .name, 'say' , this .word].join(' ' ); console .log(talk); } var bottle = { name : 'bottle' , word : 'hello' }; sayWord.call(bottle);
所以,call
主要实现了以下两个功能:
call
改变了 this
的指向bottle
执行了 sayWord
函数模拟实现 call 模拟实现 call
有三步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Function .prototype.call = function (context ) { context = context ? Object (context) : window ; context.fn = this ; let args = [...arguments].slice(1 ); let result = context.fn(...args); delete context.fn return result; }
Function.prototype.apply() apply()
方法调用一个具有给定 this
值的函数,以及作为一个数组(或[类似数组对象)提供的参数。
1 func.apply(thisArg, [argsArray])
它运行 func
设置 this = context
并使用类数组对象 args
作为参数列表。
例如,这两个调用几乎相同:
1 2 func(1 , 2 , 3 ); func.apply(context, [1 , 2 , 3 ])
两个都运行 func
给定的参数是 1,2,3
。但是 apply
也设置了 this = context
。
call
和 apply
之间唯一的语法区别是 call
接受一个参数列表,而 apply
则接受带有一个类数组对象。
需要注意:Chrome 14 以及 Internet Explorer 9 仍然不接受类数组对象。如果传入类数组对象,它们会抛出异常。
模拟实现 apply 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Function .prototype.apply = function (context, arr ) { context = context ? Object (context) : window ; context.fn = this ; let result; if (!arr) { result = context.fn(); } else { result = context.fn(...arr); } delete context.fn return result; }
解析 bind 原理,并手写 bind 实现 bind() bind()
方法创建一个新的函数,在 bind()
被调用时,这个新函数的 this
被指定为 bind()
的第一个参数,而其余参数将作为新函数的参数,供调用时使用。
— MDN
bind
方法与 call / apply
最大的不同就是前者返回一个绑定上下文的函数 ,而后两者是直接执行 了函数。
来个例子说明下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 let value = 2 ;let foo = { value : 1 }; function bar (name, age ) { return { value : this .value, name : name, age : age } }; bar.call(foo, "Jack" , 20 ); let bindFoo1 = bar.bind(foo, "Jack" , 20 ); bindFoo1(); let bindFoo2 = bar.bind(foo, "Jack" ); bindFoo2(20 );
通过上述代码可以看出 bind
有如下特性:
1、指定 this
2、传入参数 3、返回一个函数 4、柯里化 模拟实现: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Function .prototype.bind = function (context ) { if (typeof this !== "function" ) { throw new Error ("Function.prototype.bind - what is trying to be bound is not callable" ); } var self = this ; var args = Array .prototype.slice.call(arguments , 1 ); return function ( ) { var bindArgs = Array .prototype.slice.call(arguments ); return self.apply( context, args.concat(bindArgs) ); } }
但还有一个问题,bind
有以下一个特性:
一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。
来个例子说明下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let value = 2 ;let foo = { value : 1 }; function bar (name, age ) { this .habit = 'shopping' ; console .log(this .value); console .log(name); console .log(age); } bar.prototype.friend = 'kevin' ; let bindFoo = bar.bind(foo, 'Jack' );let obj = new bindFoo(20 );obj.habit; obj.friend;
上面例子中,运行结果 this.value
输出为 undefined
,这不是全局 value
也不是 foo
对象中的 value
,这说明 bind
的 this
对象失效了,new
的实现中生成一个新的对象,这个时候的 this
指向的是 obj
。
这个可以通过修改返回函数的原型来实现,代码如下:
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 .prototype.bind = function (context ) { if (typeof this !== "function" ) { throw new Error ("Function.prototype.bind - what is trying to be bound is not callable" ); } var self = this ; var args = Array .prototype.slice.call(arguments , 1 ); var fNOP = function ( ) {}; var fBound = function ( ) { var bindArgs = Array .prototype.slice.call(arguments ); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this .prototype; fBound.prototype = new fNOP(); return fBound; }
注释1 :
当作为构造函数时,this
指向实例,此时 this instanceof fBound
结果为 true
,可以让实例获得来自绑定函数的值,即上例中实例会具有 habit
属性。 当作为普通函数时,this
指向 window
,此时结果为 false
,将绑定函数的 this
指向 context
注释2 :
修改返回函数的 prototype
为绑定函数的 prototype
,实例就可以继承绑定函数的原型中的值,即上例中 obj
可以获取到 bar
原型上的 friend
至于为什么使用一个空对象 fNOP
作为中介,把 fBound.prototype
赋值为空对象的实例(原型式继承),这是因为直接 fBound.prototype = this.prototype
有一个缺点,修改 fBound.prototype
的时候,也会直接修改 this.prototype
;其实也可以直接使用ES5的 Object.create()
方法生成一个新对象,但 bind
和 Object.create()
都是ES5方法,部分IE浏览器(IE < 9)并不支 注意: bind()
函数在 ES5 才被加入,所以并不是所有浏览器都支持,IE8
及以下的版本中不被支持,如果需要兼容可以使用 Polyfill 来实现
详情可前往 深度解析bind原理、使用场景及模拟实现 查看
补充:柯里化 在计算机科学中,柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。这个技术由 Christopher Strachey 以逻辑学家 Haskell Curry 命名的,尽管它是 Moses Schnfinkel 和 Gottlob Frege 发明的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 var add = function (x ) { return function (y ) { return x + y; }; }; var increment = add(1 );var addTen = add(10 );increment(2 ); addTen(2 ); add(1 )(2 );
这里定义了一个 add
函数,它接受一个参数并返回一个新的函数。调用 add
之后,返回的函数就通过闭包的方式记住了 add
的第一个参数。所以说 bind
本身也是闭包的一种使用场景。
柯里化 是将 f(a,b,c)
可以被以 f(a)(b)(c)
的形式被调用的转化。JavaScript 实现版本通常保留函数被正常调用和在参数数量不够的情况下返回偏函数这两个特性。
节流 与 防抖 debounce
与 throttle
是开发中常用的高阶函数,作用都是为了防止函数被高频调用,换句话说就是,用来控制某个函数在一定时间内执行多少次。
使用场景 比如绑定响应鼠标移动、窗口大小调整、滚屏等事件时,绑定的函数触发的频率会很频繁。若处理函数稍微复杂,需要较多的运算执行时间和资源,往往会出现延迟,甚至导致假死或者卡顿感。为了优化性能,这时就很有必要使用 debounce
或 throttle
了。
debounce 与 throttle 区别 防抖 (debounce ) :多次触发,只在最后一次触发 时,执行目标函数。
节流(throttle ):限制目标函数调用的频率 ,比如:1s内不能调用2次。
手写一个 throttle 实现方案有以下两种:
第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差(Xms) ,如果是则执行,并更新上次执行的时间戳,如此循环。 第二种方法是使用定时器,比如当 scroll
事件刚触发时,打印一个 hello world ,然后设置个 1000ms
的定时器,此后每次触发 scroll
事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler
被清除,然后重新设置定时器。 这里我们采用第一种方案来实现,通过闭包保存一个 previous
变量,每次触发 throttle
函数时判断当前时间和 previous
的时间差,如果这段时间差小于等待时间,那就忽略本次事件触发。如果大于等待时间就把 previous
设置为当前时间并执行函数 fn。
我们来一步步实现,首先实现用闭包保存 previous
变量。
1 2 3 4 5 6 7 8 const throttle = (fn, wait ) => { let previous = 0 return function (...args ) { console .log(previous) ... } }
执行 throttle
函数后会返回一个新的 function
,我们命名为 betterFn
。
1 2 3 4 const betterFn = function (...args ) { console .log(previous) ... }
betterFn
函数中可以获取到 previous
变量值也可以修改,在回调监听或事件触发时就会执行 betterFn
,即 betterFn()
,所以在这个新函数内判断当前时间和 previous
的时间差即可。
1 2 3 4 5 6 7 8 const betterFn = function (...args ) { let now = +new Date (); if (now - previous > wait) { previous = now fn.apply(this , args) } }
结合上面两段代码就实现了节流函数,所以完整的实现如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const throttle = (fn, wait = 50 ) => { let previous = 0 ; return function (...args ) { let now = +new Date (); if (now - previous > wait) { previous = now; fn.apply(this , args); } } } const betterFn = throttle(() => console .log('fn 函数执行了' ), 1000 )setInterval (betterFn, 10 )
手写一个防抖函数 debounce 防抖函数 debounce
指的是某个函数在某段时间内,无论触发了多少次回调,都只执行最后一次
实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。
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 function debounce (fn, wait = 50 ) { let timer = null return function (...args ) { const context = this if (timer) clearTimeout (timer) timer = setTimeout (() => { fn.apply(context, args) }, wait) } } const betterFn = debounce(() => console .log('fn 防抖执行了' ), 1000 )document .addEventListener('scroll' , betterFn)
不过 underscore
中的 debounce
还有第三个参数:immediate
。这个参数是做什么用的呢?
传参 immediate 为 true, debounce 会在 wait 时间间隔的开始调用这个函数 。(注:并且在 wait 的时间之内,不会再次调用。)在类似不小心点了提交按钮两下而提交了两次的情况下很有用。
把 true
传递给 immediate
参数,会让 debounce
在 wait
时间开始计算之前就触发函数(也就是没有任何延时就触发函数),而不是过了 wait
时间才触发函数,而且在 wait
时间内也不会触发(相当于把 fn
的执行锁住)。 如果不小心点了两次提交按钮,第二次提交就会不会执行。
那我们根据 immediate
的值来决定如何执行 fn
。如果是 immediate
的情况下,我们立即执行 fn
,并在 wait
时间内锁住 fn
的执行, wait
时间之后再触发,才会重新执行 fn
,以此类推。
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 function debounce (fn, wait = 50 , immediate ) { let timer = null ; return function (...args ) { const context = this ; if (timer) clearTimeout (timer) if (immediate && !timer) { fn.apply(context, args); } timer = setTimeout (() => { fn.apply(context, args); }, wait) } } const betterFn = debounce(() => console .log('fn 防抖执行了' ), 1000 , true )document .addEventListener('scroll' , betterFn)
继承 原型链继承 1 2 3 4 5 6 7 8 9 10 11 12 function Parent ( ) { this .name = 'kk' ; } Parent.prototype.getName = function ( ) { console .log(this .name); } function Child ( ) {} Child.prototype = new Parent(); let child1 = new Child();console .log(child1.getName);
借用构造函数(经典继承) 1 2 3 4 5 6 7 8 9 10 11 function Parent ( ) { this .name = ['kk' ,'ll' ]; } function Child ( ) { Parent.call(this ); } let child2 = new Child();child2.names.push('yayu' ); console .log(child2.names); let child3 = new Child();console .log(child3.names);
组合继承 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 function Parent (name ) { this .name = name; this .colors = ['red' , 'blue' , 'green' ]; } Parent.prototype.getName = function ( ) { console .log(this .name) } function Child (name, age ) { Parent.call(this , name); this .age = age; } Child.prototype = new Parent(); var child1 = new Child('kevin' , '18' );child1.colors.push('black' ); console .log(child1.name); console .log(child1.age); console .log(child1.colors); var child2 = new Child('daisy' , '20' );console .log(child2.name); console .log(child2.age); console .log(child2.colors);
Class实现继承 1 2 3 4 5 6 7 8 9 10 11 12 13 14 class Animal { constructor (name ) { this .name = name } getName ( ) { return this .name } } class Dog extends Animal { constructor (name, age ) { super (name) this .age = age } }
Promise 在传统的异步编程中,如果异步之间存在依赖关系,我们就需要通过层层嵌套回调来满足这种依赖,如果嵌套层数过多,可读性和可维护性都变得很差,产生所谓“回调地狱”,而Promise将回调嵌套改为链式调用,增加可读性和可维护性。
观察者模式 我们先来看一个最简单的Promise使用:
1 2 3 4 5 6 7 const p1 = new Promise ((resolve, reject ) => { setTimeout (() => { resolve('result' ) },1000 ); }) p1.then(res => console .log(res), err => console .log(err))
观察这个例子,我们分析Promise的调用流程:
Promise
的构造方法接收一个executor()
,在new Promise()
时就立刻执行这个executor回调executor()
内部的异步任务被放入宏/微任务队列,等待执行then()
被执行,收集成功/失败回调,放入成功/失败队列executor()
的异步任务被执行,触发resolve/reject
,从成功/失败队列中取出回调依次执行其实熟悉设计模式的同学,很容易就能意识到这是个观察者模式 ,这种收集依赖 -> 触发通知 -> 取出依赖执行
的方式,被广泛运用于观察者模式的实现,在Promise里,执行顺序是then收集依赖 -> 异步触发resolve -> resolve执行依赖
。
Promise A+规范 上面我们已经简单地实现了一个超低配版Promise,但我们会看到很多文章和我们写的不一样,他们的Promise实现中还引入了各种状态控制,这是由于ES6的Promise实现需要遵循Promise/A+规范 ,是规范对Promise的状态控制做了要求。Promise/A+的规范比较长,这里只总结两条核心规则:
Promise本质是一个状态机,且状态只能为以下三种:Pending(等待态)
、Fulfilled(执行态)
、Rejected(拒绝态)
,状态的变更是单向的,只能从Pending -> Fulfilled 或 Pending -> Rejected,状态变更不可逆 then方法
接收两个可选参数,分别对应状态改变时触发的回调。then方法返回一个promise。then 方法可以被同一个 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 31 32 33 34 35 36 37 38 39 40 41 42 43 const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class MyPromise { constructor (executor ) { this ._status = PENDING this ._resolveQueue = [] this ._rejectQueue = [] let _resolve = (val ) => { if (this ._status !== PENDING) return this ._status = FULFILLED while (this ._resolveQueue.length) { const callback = this ._resolveQueue.shift() callback(val) } } let _reject = (val ) => { if (this ._status !== PENDING) return this ._status = REJECTED while (this ._rejectQueue.length) { const callback = this ._rejectQueue.shift() callback(val) } } executor(_resolve, _reject) } then (resolveFn, rejectFn ) { this ._resolveQueue.push(resolveFn) this ._rejectQueue.push(rejectFn) } }
then的链式调用 补充完规范,我们接着来实现链式调用,这是Promise实现的重点和难点
我们思考一下如何实现这种链式调用:
显然.then()
需要返回一个Promise,这样才能找到then方法,所以我们会把then方法的返回值包装成Promise。 .then()
的回调需要拿到上一个.then()
的返回值.then()
的回调需要顺序执行,以上面这段代码为例,虽然中间return了一个Promise,但执行顺序仍要保证是1->2->3。我们要等待当前Promise状态变更后,再执行下一个then收集的回调,这就要求我们对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 then (resolveFn, rejectFn ) { return new MyPromise((resolve, reject ) => { const fulfilledFn = value => { try { let x = resolveFn(value) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } this ._resolveQueue.push(fulfilledFn) const rejectedFn = error => { try { let x = rejectFn(error) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } this ._rejectQueue.push(rejectedFn) }) }
值穿透 & 状态已变更的情况 我们已经初步完成了链式调用,但是对于 then() 方法,我们还要两个细节需要处理一下
值穿透 :根据规范,如果 then() 接收的参数不是function,那么我们应该忽略它。如果没有忽略,当then()回调不为function时将会抛出异常,导致链式调用中断处理状态为resolve/reject的情况 :其实我们上边 then() 的写法是对应状态为padding
的情况,但是有些时候,resolve/reject 在 then() 之前就被执行(比如Promise.resolve().then()
),如果这个时候还把then()回调push进resolve/reject的执行队列里,那么回调将不会被执行,因此对于状态已经变为fulfilled
或rejected
的情况,我们直接执行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 then (resolveFn, rejectFn ) { typeof resolveFn !== 'function' ? resolveFn = value => value : null typeof rejectFn !== 'function' ? rejectFn = reason => { throw new Error (reason instanceof Error ? reason.message:reason); } : null return new MyPromise((resolve, reject ) => { const fulfilledFn = value => { try { let x = resolveFn(value) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } const rejectedFn = error => { try { let x = rejectFn(error) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } switch (this ._status) { case PENDING: this ._resolveQueue.push(fulfilledFn) this ._rejectQueue.push(rejectedFn) break ; case FULFILLED: fulfilledFn(this ._value) break ; case REJECTED: rejectedFn(this ._value) break ; } }) }
兼容同步任务 完成了then的链式调用以后,我们再处理一个前边的细节,然后放出完整代码。上文我们说过,Promise的执行顺序是new Promise -> then()收集回调 -> resolve/reject执行回调
,这一顺序是建立在executor是异步任务 的前提上的,如果executor是一个同步任务,那么顺序就会变成new Promise -> resolve/reject执行回调 -> then()收集回调
,resolve的执行跑到then之前去了,为了兼容这种情况,我们给resolve/reject
执行回调的操作包一个setTimeout,让它异步执行。
这里插一句,有关这个setTimeout,其实还有一番学问。虽然规范没有要求回调应该被放进宏任务队列还是微任务队列,但其实Promise的默认实现是放进了微任务队列,我们的实现(包括大多数Promise手动实现和polyfill的转化)都是使用setTimeout放入了宏任务队列(当然我们也可以用MutationObserver模拟微任务)
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 const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class MyPromise { constructor (executor ) { this ._status = PENDING this ._value = undefined this ._resolveQueue = [] this ._rejectQueue = [] let _resolve = (val ) => { const run = () => { if (this ._status !== PENDING) return this ._status = FULFILLED this ._value = val while (this ._resolveQueue.length) { const callback = this ._resolveQueue.shift() callback(val) } } setTimeout (run) } let _reject = (val ) => { const run = () => { if (this ._status !== PENDING) return this ._status = REJECTED this ._value = val while (this ._rejectQueue.length) { const callback = this ._rejectQueue.shift() callback(val) } } setTimeout (run) } executor(_resolve, _reject) } then (resolveFn, rejectFn ) { typeof resolveFn !== 'function' ? resolveFn = value => value : null typeof rejectFn !== 'function' ? rejectFn = reason => { throw new Error (reason instanceof Error ? reason.message:reason); } : null return new MyPromise((resolve, reject ) => { const fulfilledFn = value => { try { let x = resolveFn(value) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } const rejectedFn = error => { try { let x = rejectFn(error) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } switch (this ._status) { case PENDING: this ._resolveQueue.push(fulfilledFn) this ._rejectQueue.push(rejectedFn) break ; case FULFILLED: fulfilledFn(this ._value) break ; case REJECTED: rejectedFn(this ._value) break ; } }) } }
Promise.prototype.catch() catch()方法
返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。
1 2 3 4 5 catch (rejectFn) { return this .then(undefined , rejectFn) } 复制代码
Promise.prototype.finally() finally()方法
返回一个Promise。在promise结束时,无论结果是fulfilled或者是rejected,都会执行指定的回调函数。在finally之后,还可以继续then。并且会将值原封不动的传递给后面的then
1 2 3 4 5 6 7 8 finally (callback ) { return this .then( value => MyPromise.resolve(callback()).then(() => value), reason => MyPromise.resolve(callback()).then(() => { throw reason }) ) } 复制代码
PS. 有同学问我MyPromise.resolve(callback())
的意义,这里补充解释一下:这个写法其实涉及到一个finally()
的使用细节,finally()如果return了一个reject状态的Promise,将会改变当前Promise的状态 ,这个MyPromise.resolve
就用于改变Promise状态,在finally()没有返回reject态Promise或throw错误的情况下,去掉MyPromise.resolve
也是一样的(欢迎大家向我提问,勘误的过程中也能很好地加深自己对Promise的理解,大家可以在各个交流群里直接@我)
参考资料:对 Promise.prototype.finally() 的粗浅理解
Promise.resolve() Promise.resolve(value)
方法返回一个以给定值解析后的Promise 对象。如果该值为promise,返回这个promise;如果这个值是thenable(即带有”then” 方法)),返回的promise会“跟随”这个thenable的对象,采用它的最终状态;否则返回的promise将以此值完成。此函数将类promise对象的多层嵌套展平。
1 2 3 4 5 static resolve (value ) { if (value instanceof MyPromise) return value return new MyPromise(resolve => resolve(value)) }
Promise.reject() Promise.reject()
方法返回一个带有拒绝原因的Promise对象。
1 2 3 4 static reject (reason ) { return new MyPromise((resolve, reject ) => reject(reason)) }
Promise.all() Promise.all(iterable)
方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失败原因的是第一个失败 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 static all (promiseArr ) { let index = 0 let result = [] return new MyPromise((resolve, reject ) => { promiseArr.forEach((p, i ) => { MyPromise.resolve(p).then( val => { index++ result[i] = val if (index === promiseArr.length) { resolve(result) } }, err => { reject(err) } ) }) }) }
Promise.race() Promise.race(iterable)
方法返回一个 promise,一旦迭代器中的某个promise解决或拒绝,返回的 promise就会解决或拒绝。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 static race (promiseArr ) { return new MyPromise((resolve, reject ) => { for (let p of promiseArr) { MyPromise.resolve(p).then( value => { resolve(value) }, err => { reject(err) } ) } }) }
完整代码 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 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' class MyPromise { constructor (executor ) { this ._status = PENDING this ._value = undefined this ._resolveQueue = [] this ._rejectQueue = [] let _resolve = (val ) => { const run = () => { if (this ._status !== PENDING) return this ._status = FULFILLED this ._value = val while (this ._resolveQueue.length) { const callback = this ._resolveQueue.shift() callback(val) } } setTimeout (run) } let _reject = (val ) => { const run = () => { if (this ._status !== PENDING) return this ._status = REJECTED this ._value = val while (this ._rejectQueue.length) { const callback = this ._rejectQueue.shift() callback(val) } } setTimeout (run) } executor(_resolve, _reject) } then (resolveFn, rejectFn ) { typeof resolveFn !== 'function' ? resolveFn = value => value : null typeof rejectFn !== 'function' ? rejectFn = reason => { throw new Error (reason instanceof Error ? reason.message:reason); } : null return new MyPromise((resolve, reject ) => { const fulfilledFn = value => { try { let x = resolveFn(value) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } const rejectedFn = error => { try { let x = rejectFn(error) x instanceof MyPromise ? x.then(resolve, reject) : resolve(x) } catch (error) { reject(error) } } switch (this ._status) { case PENDING: this ._resolveQueue.push(fulfilledFn) this ._rejectQueue.push(rejectedFn) break ; case FULFILLED: fulfilledFn(this ._value) break ; case REJECTED: rejectedFn(this ._value) break ; } }) } catch (rejectFn) { return this .then(undefined , rejectFn) } finally (callback ) { return this .then( value => MyPromise.resolve(callback()).then(() => value), reason => MyPromise.resolve(callback()).then(() => { throw reason }) ) } static resolve (value ) { if (value instanceof MyPromise) return value return new MyPromise(resolve => resolve(value)) } static reject (reason ) { return new MyPromise((resolve, reject ) => reject(reason)) } static all (promiseArr ) { let index = 0 let result = [] return new MyPromise((resolve, reject ) => { promiseArr.forEach((p, i ) => { MyPromise.resolve(p).then( val => { index++ result[i] = val if (index === promiseArr.length) { resolve(result) } }, err => { reject(err) } ) }) }) } static race (promiseArr ) { return new MyPromise((resolve, reject ) => { for (let p of promiseArr) { MyPromise.resolve(p).then( value => { resolve(value) }, err => { reject(err) } ) } }) } }
Ajax 什么是Ajax和JSON,它们的优缺点 Ajax是全称是Asynchronous JavaScritpt and XML
,即异步JavaScript和xml,用于在Web页面中实现异步数据交互 ,实现页面局部刷新 。
优点:可以实现异步通信效果,页面局部刷新,带来更好的用户体验。
JSON是一种轻量级的数据交换格式,看着像对象,本质是字符串 。
优点:轻量级、易于人的阅读和编写,便于js解析,支持复合数据类型。
介绍一下XMLHttpRequest对象,他有哪些常用方法和属性 XMLHttpRequest
是Ajax的核心,通过XMLHttpRequest对象,Web开发人员可以在页面加载以后进行页面的局部更新
常用的方法:
open(get/post,url,是否异步)创建http请求 send()发送请求给服务器 setRequestHeader()设置头信息(使用post才会用到,get并不需要调用该方法) 常用的属性:
onreadystatechange 用于监听ajax的工作状态(readyState变化时会调用此方法) readyState 用来存放XMLHttpRequest的状态 status 服务器返回的状态码 responseText 服务器返回的文本内容 说下readyState属性是干嘛的,都有哪几个状态 readyState属性用来存放XMLHttpRequest的状态,监听从0-4发生不同的变化
0:请求未初始化(此时还没有调用open)
1:服务器连接已建立,已经发送请求开始监听
2:请求已接收,已经收到服务器返回的内容
3:请求处理中,解析服务器响应内容
4:请求已完成,且响应就绪
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 const ajax = url => { return new Promise ((resolve,reject )=> { const xhr = new XMLHttpRequest(); xhr.responseType = 'json' ; xhr.open("GET" ,url); xhr.send(); xhr.onreadystatechange = () => { if (xhr.readyState === 4 ){ if (xhr.status >= 200 && xhr.status <300 ){ resolve(xhr.response); }else { reject(xhr.status); } } } }) } let btn = document .querySelector('#btn' );btn.addEventListener('click' ,async function ( ) { let duanzi = await sendAJAX('https://api.apiopen.top/getJoke' ); console .log(duanzi); });
axios封装
前端最流行的 ajax 请求库 react/vue 官方都推荐使用 axios 发 ajax 请求 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 function ajax (url,data={},type='GET' ) { return new Promise ((resolve,reject ) => { let promise; if (type==='GET' ){ promise=axios.get(url,{ params :data }) }else { promise=axios.post(url,data) } promise.then(response => { resolve(response.data) }).catch(error => { message.error('请求出错了:' +error.message) }) }) }
高阶函数 什么样的函数是高阶函数那?
至少满足下列一个条件的函数:
JavaScript
中的高阶函数大多都是接受一个函数作为参数,具体传参如下:
1 2 Array .prototype.func = function (callback(currentValue[, index[, array]]){ }[, thisArg] )
callback
:对数组元素进行操作的回调函数currentValue
:正在处理的当前元素index
:当前元素的索引array
:调用高阶函数的数组thisArg
:可选,执 行callback
函数时绑定的 this
forEach 用法 forEach
主要用于数组的简单遍历,基本使用如下
1 2 3 4 5 6 7 8 arr = [1 , 2 , 3 , 4 ] arr.forEach((val, index ) => { console .log(val, index) }) for (var i = 0 ; i<arr.length; i++) { console .log(arr[i], i) }
模拟实现 我们先来回想一下上面案例中,forEach
内部发生了什么,很简单,就是类似 for
循环一样,运行 arr.length
次回调函数,回调函数的参数是对应的元素索引、元素值和数组本身。那我们就可以模拟出大概的运行流程。
1 2 3 for (var i = 0 ; i<arr.length; i++) { callback(arr[i], i, arr) }
由于 forEach
还可以接受 thisArg
参数作为回调函数的上下文环境,因此使用 call/apply
对上面代码略作修改。
1 callback.call(thisArg, arr[i], i, arr)
通过上面分析,我们就可写出完整的模拟代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Array .prototype.myForEach = function (callbackFn ) { if (this === null || this === undefined ) { throw new TypeError ("Cannot read property 'myForEach' of null" ); } if (Object .prototype.toString.call(callbackFn) !== "[object Function]" ) { throw new TypeError (callbackFn + ' is not a function' ) } var _arr = this , thisArg = arguments [1 ] || window ; for (var i = 0 ; i < _arr.length; i++) { callbackFn.call(thisArg, _arr[i], i, _arr); } } 复制代码
map 用法 map
函数对数组的每个元素执行回调函数,并返回含有运算结果的新数组,基本使用如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const users = [ { name : 'John' , age : 34 }, { name : 'Amy' , age : 20 }, { name : 'camperCat' , age : 10 } ]; names = [] for (var i = 0 ; i<users.length; i++){ names.push(users[i].name) } names = users.map(function (user ) { return user.name }) names = user.map(user => user.name)
模拟实现 有了上面 forEach
的编写经验,map
只需要稍作修改,使其结果返回新的数组(这里省略掉异常判断)。
1 2 3 4 5 6 7 8 Array .prototype.myMap = function (callbackFn ) { var _arr = this , thisArg = arguments [1 ] || window , res = []; for (var i = 0 ; i<_arr.length; i++) { res.push(callbackFn.call(thisArg, _arr[i], i, _arr)); } return res; }
reduce 用法 reduce
与前面的方法有略微的差别:
1 2 arr.reduce(callback (accumulator, currentValue[, index[, array]] ) { }[, initialValue])
callback
:对数组元素进行操作的回调函数accumulator
:累计器累计回调的返回值; 它是上一次调用回调时返回的累积值,或initialValue
currentValue
:正在处理的当前元素当前元素的索引 调用高阶函数的数组 initialValue
:作为第一次调用函数的初始值。如果没有提供初始值,则使用数组中的第一个元素。没有初始值的空数组调用 reduce 会报错
可累加的效果为 reduce 增添了很多精彩,也产生了很多很有用的用途。
数组累加和 1 2 arr = [0 , 1 , 2 , 3 , 4 ] arr.reduce((accu, current ) => accu + current, 0 )
累加对象数组和 这里如果只是像上面一样使用reduce,最终的结果会存在问题
1 2 3 objArr = [{x : 1 }, {x :2 }, {x :3 }] objArr.reduce((accu, current ) => accu.x + current.x, 0 ) 复制代码
上述代码返回的结果为NaN ,为什么那?
上文提过 accumulator
它的值为上一次调用之后的累计值或初始值,因此第一次调用过后将3赋值给 accumulator
,不再具有 x
属性,因此最终返回 NaN
1 2 3 4 objArr.map(obj => obj.x).((accu, current ) => accu + current, 0 ) objArr.reduce((accu, current ) => accu + current.x, 0 )
计算数组中每个元素出现的次数 1 2 3 4 5 6 7 8 9 10 11 12 var names = ['Alice' , 'Bob' , 'Tiff' , 'Bruce' , 'Alice' ];var countedNames = names.reduce(function (allNames, name ) { if (name in allNames) { allNames[name]++; } else { allNames[name] = 1 ; } return allNames; }, {}); 复制代码
模拟实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Array .prototype.myReduce = function (callbackFn ) { var _arr = this , accumulator = arguments [1 ]; var i = 0 ; if (accumulator === undefined ) { if (_arr.length === 0 ) { throw new Error ('initVal and Array.length>0 need one' ) } accumulator = _arr[i]; i++; } for (; i<_arr.length; i++) { accumulator = callbackFn(accumulator, _arr[i], i, _arr) } return accumulator; }
filter 用法 filter
是过滤的意思,它对数组每个元素执行回调函数,返回回调函数执行结果为true
的元素。
1 2 3 arr = [1 , 2 , 3 , 4 , 5 ]; arr.filter(val => val % 2 == 0 )
模拟实现 与 map
的实现大同小异,map
返回执行回调后所有的元素,而 filter
只返回回调函数执行结果为 true
的元素。
1 2 3 4 5 6 7 8 9 10 Array .prototype.myFilter = function (callbackFn ) { var _arr = this , thisArg = arguments [1 ] || window , res = []; for (var i = 0 ; i<_arr.length; i++) { if (callbackFn.call(thisArg, _arr[i], i, _arr)) { res.push(_arr[i]); } } return res; }
every 用法 every
并不返回数组,返回布尔值 true/false
,数组的每个元素执行回调函数,如果执行结果全为 true
,every
返回 true
,否则返回 false
。
1 2 3 4 5 6 7 arr = [1 , 3 , 5 , 7 , 8 , 9 ] arr.every(ele => ele % 2 == 1 ) arr2 = [2 , 4 , 6 ] arr2.every(ele => ele % 2 == 0 )
模拟实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Array .prototype.myEvery = function (callbackFn ) { var _arr = this , thisArg = arguments [1 ] || window ; var flag = true ; for (var i = 0 ; i<_arr.length; i++) { if (!callbackFn.call(thisArg, _arr[i], i, _arr)) { return false ; } } return flag; }
some 用法 some
与 every
功能类似,都是返回布尔值。只要回调函数结果有一个 true
,some
便返回 true
,否则返回 false
。
模拟实现 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Array .prototype.mySome = function (callbackFn ) { var _arr = this , thisArg = arguments [1 ] || window ; var flag = false ; for (var i = 0 ; i<_arr.length; i++) { if (callbackFn.call(thisArg, _arr[i], i, _arr)) { return true ; } } return flag; }
find/findIndex 用法 find
与 findIndex
是 ES6
新添加的数组方法,返回满足回调函数的第一个数组元素/数组元素索引。当数组中没有能满足回调函数的元素时,会分别返回 undefined和-1
。
1 2 3 4 5 const users = [ { name : 'John' , age : 34 }, { name : 'Amy' , age : 20 }, { name : 'camperCat' , age : 10 } ];
返回name为John的年龄
在没有find方法时,实现类似效果,需要循环遍历,查找到name=Jonn后,找到年龄。但使用find就可以轻松快捷的实现。
1 JohnAge = users.find(user => user.name === 'John' ).age
返回name为Amy的索引
ES6以前Array提供了查找数组中元素的方法:indexOf,lastIndexOf,但是这两个方法在查找对象时都无能为力。
1 2 3 4 users.indexOf({ name : 'Amy' , age : 20 }) users.findIndex(user => user.name === 'Amy' )
indexOf也可以用来查找数组中是否存在某元素,但其语义化并不好,每次需要与 -1 进行比较,因此 ES6 添加了新的 includes 方法。
模拟实现 find/findIndex
都是寻找到第一个满足回调函数的元素返回,上面我们学习的 some
也是类似机制,因此它们的原生代码类似。
1 2 3 4 5 6 7 8 9 10 11 12 Array .prototype.myFind = function (callbackFn ) { var _arr = this , thisArg = arguments [1 ] || window ; for (var i = 0 ; i<_arr.length; i++) { if (callbackFn.call(thisArg, _arr[i], i, _arr)) { return _arr[i]; } } return undefined ; }