本文最后更新于:8 个月前
JS常见函数的语法、用法、以及模拟实现。包括:call、apply、bind filter、map、reduce instanceof、new
JavaScript进阶(四)手写JS经典函数 一、call call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数。——MDN
1.1 call 语法 1 2 fn.call (thisArg,arg1, [,arg2,...])
参数:
thisArg: (可选)需要指定的上下文对象,可选,不指定或者指定为null、undefined时,为全局对象
后续参数 :(可选)从第二个参数起,作为调用函数的参数,数量不固定
返回值: 函数fn的返回值
1.2 手写模拟call 模拟实现流程:
将函数设为thisArg
对象的属性
执行该函数
删除该函数
需要考虑的两个问题:
this参数可能为null,当this为null时,视为指向window
需要返回函数结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Function .prototype .myCall = function (context,...args ){ if (!context) context = window context.fn = this const result = context.fn (...args) delete context.fn return result }var obj = {age :'23' }var age = '45' function log (age ){ console .log (this .age ,age) } log.myCall (obj,24 ) log.call (obj,24 )
二、apply apply()方法调用一个具有给定 this值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。
2.1 apply语法 1 2 3 4 5 fn.apply (thisArg, argsArray)
参数:
thisArg: (可选)需要指定的上下文对象,可选,不指定或者指定为null、undefined时,为全局对象
argsArray: (可选)参数数组,Array类型,数组内部的所有元素作为函数fn的调用参数
返回值: 函数fn的返回值
2.2 手写模拟apply 模拟实现流程: (与call相同,区别在于参数的形式,call是单独的参数,apply是参数数组)
将函数设为对象的属性。通过调用thisArg对象来调用函数,从而实现this指向thisArg的目的
执行该函数
删除该函数
需要考虑的问题: thisArg可能为null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 Function .prototype .myApply = function (context,argsArr ){ context = context || window context.fn = this const result = context.fn (...argsArr) delete context.fn return result }var value = 1 var obj = {value :10 }function log (name,age ){ console .log (this .value ,name,age) } log.myApply (obj,['Q' ,24 ]) log.apply (obj,['Q' ,24 ])
三、bind apply和call 的实现都较为简单。bind 的实现相对比较复杂,因为它除了绑定this的作用外,还有设置偏函数的作用。而且bind绑定了一次this对象之后,就不能再修改了。
bind()方法创建一个新的函数 ,在 bind() 被调用时,这个新函数的第一个参数将作为它运行时的this,而其余参数将作为新函数的参数,供调用时使用。this只能绑定一次,第一次绑定this即为永久性绑定 ,后面无法再通过bind、apply、call更改
3.1 bind语法 1 2 fn.bind (thisArg, arg1 [,arg2,...])
参数:
thisArg: (可选)函数运行时的this对象,当thisArg缺省或者为null、undefined时,函数运行时的this对象取决于新函数的this对象
arg1 [,arg2,…]: (可选)被预置入绑定函数的参数列表中的参数
返回值: 原函数的拷贝,并且拥有指定的 this 对象 和 初始函数
bind() 函数会创建一个新的绑定函数 (bound function ,BF)。绑定函数是一个 exotic function object(怪异函数对象,ECMAScript 2015 中的术语),它包装了原函数对象。调用绑定函数 通常会导致执行包装函数 。 绑定函数 具有以下内部属性:
[[BoundTargetFunction]] - 包装的函数对象
[[BoundThis]] - 在调用包装函数时始终作为 this 值传递的值。
[[BoundArguments]] - 列表,在对包装函数做任何调用都会优先用列表元素填充参数列表。
[[Call]] - 执行与此对象关联的代码。通过函数调用表达式调用。内部方法的参数是一个this 值和一个包含通过调用表达式传递给函数的参数的列表。
当调用绑定函数时,它调用 [[BoundTargetFunction]] 上的内部方法 **[[Call]]**,就像这样 Call(*boundThis*, *args*)。其中, boundThis 是 [[BoundThis]], args 是 [[BoundArguments]] 加上通过函数调用传入的参数列表。
绑定函数也可以使用 new
运算符构造,它会表现为目标函数已经被构建完毕了似的。提供的 this
值会被忽略,但前置参数仍会提供给模拟函数。
3.2 bind用法示例 3.2.1 创建绑定函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 this .x = 9 ; var module = { x : 81 , getX : function ( ) { return this .x ; } };var module2 = { x : 18 }module .getX () var retrieveX = module .getX retrieveX () var boundGetX = retrieveX.bind (module )boundGetX () boundGetX = boundGetX.bind (module2)boundGetX ()
3.2.2 创建偏函数 使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为 bind()
的参数写在 this
后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面
可以用来实现函数柯里化
1 2 3 4 5 6 7 8 9 function fn (a,b,c ){ return a+b+c }console .log (fn.length ) var f1 = fn.bind (null ,1 )console .log (f1) var f2 = f1.bind (null ,2 )console .log (f2) f2 (3 )
3.3 手写模拟bind 模拟实现
返回一个绑定了this的函数
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 24 25 26 27 28 Function .prototype .myBind = function (context,...args ){ var self = this return function ( ){ var bindArgs = Array .prototype .slice .call (arguments ) return self.apply (context,args.concat (bindArgs)) } }function fn (a,b,c ){ return a+b+c }var f1 = fn.myBind (null ,1 )console .log (f1.length ) var f2 = f1.myBind (null ,2 )console .log (f2) f2 (3 ) function log ( ){console .log (this .name )}var name = 'window' var obj = {name :'obj' }var fn3 = log.myBind (obj)fn3 () var obj2 = {name :'obj2' }var fn4 = fn3.bind (obj2)fn4 ()
接下来模拟实现 new+构造函数 的功能。在此之前,先了解一下bind返回的函数作为构造函数时,会发生什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 var value = 2 ;var foo = { value :1 }function bar (name,age ){ this .habit = 'shopping' console .log (this .value ) console .log (name) console .log (age) } bar.prototype .friend = 'kevin' ;var bindFoo = bar.bind (foo,'daisy' );var obj = new bindFoo ('18' );console .log (obj) console .log (obj.friend )
可以看到,绑定的this失效了,并没有指向绑定的对象、也没有指向window,实际上指向了obj。所以需要修改返回的函数的原型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Function .prototype .myBind = function (context ){ 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; }
四、filter filter()
方法创建给定数组一部分的浅拷贝 (en-US) ,其包含通过所提供函数实现的测试的所有元素
4.1 filter语法 1 arr.filter (function (element,index,array ){...},thisValue)
参数:
function(element, index, array){}
,(必须提供),提供测试判断逻辑的函数,针对每个元素进行测试,测试结果为true则加入返回的新数组
回调函数必须 return 布尔值
this.Value
,(可选),传递给函数时,用来指定this。如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象(非严格模式),严格模式下为undefined
返回值: 由通过测试条件的元素组成的新数组(浅拷贝)
4.2 filter的边界处理 数组在遍历过程中发生变化时的情况:
遍历过程中,新添加的元素不访问
遍历过程中,修改了已经被访问过的元素对最终返回结果没有影响
遍历过程中,修改或者删除即将访问(还未访问)的元素,被删除的元素访问不到,被修改的访问到的是修改后的元素值
1 2 3 4 5 6 7 8 let words = ['spray' , 'limit' , 'exuberant' , 'destruction' , 'elite' , 'present' ];const modifiedWords1 = words.filter ((word, index, arr ) => { arr[index + 1 ] += ' extra' return word.length < 6 ; });
要同时实现这3点,很复杂。后面模拟的时候并没有把边界情况实现。这里仅供了解。
4.3 手写模拟filter 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Array .prototype .myFilter = function (fn,thisArg ){ if (typeof fn !== 'function' ) return var array = this var res = [] for (var i=0 ;i<array.length ;i++){ if (fn.call (thisArg,array[i],i)){ res.push (array[i]) } } return res }var res = [11 ,5 ,3 ].myFilter (x => x<10 )
五、map map,创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。调用map函数不会对原数组产生影响,除非在回调函数的第三个参数array
,即数组引用那里对数组进行修改。
5.1 map语法 1 arr.map (function (element,index,array ){...},thisValue)
参数:
function(element,index,array)
,(必须),数组中每个元素都会执行这个回调函数,并将函数返回值组成新数组
回调函数必须return返回值!
thisValue
,(可选),传递给上面的函数,作为this。如果省略了 thisValue,或者传入 null、undefined,那么回调函数的 this 为全局对象
5.2 map的边界处理 和 filter 的边界处理一致
遍历过程中,新添加的元素不访问
遍历过程中,修改了已经被访问过的元素对最终返回结果没有影响
遍历过程中,修改或者删除即将访问(还未访问)的元素,被删除的元素访问不到,被修改的访问到的是修改后的元素值
5.3 手写模拟实现map 1 2 3 4 5 6 7 8 9 10 11 12 13 Array .prototype .myMap = function (fn,thisArg ){ if (typeof fn !== 'function' ) return const array = this var res = [] for (var i=0 ;i<array.length ;i++){ res.push ( fn.call (thisArg,array[i],i,array) ) } return res } [1 ,2 ,,4 ].myMap (x => x**3 ) [1 ,2 ,,4 ].map (x => x**3 )
六、reduce reduce的回调函数,必须设定return返回值,否则将报错! 。其实map的回调函数中也必须设置返回值,只不过即使不设置也不会报错,而是无法实现正常的功能。
6.1 reduce作用 作用: reduce,对数组中的每个元素按序执行一个由您提供的 reducer 函数,每一次运行 reducer 会将先前元素的计算结果作为参数传入,最后将其结果汇总为单个返回值
下面通过一个例子,直观了解一下reduce的执行流程
1 2 3 4 5 6 7 8 9 10 11 12 13 [1 ,2 ,3 ].reduce ((x,y )=> { console .log ('x:' ,x,', y:' ,y) return x+y; },0 )
6.2 reduce语法 1 arr.reduce (function (preValue,curValue,curIndex,array ){}, initialValue)
参数:
function(preValue, curValue, curIndex, array)
,回调函数,每个数组元素都会依次执行
preValue
,(必须),上一次调用回调函数fn的返回值。在第一次调用时,如果指定了initialValue
,为该值;如果没有指定,则为数组第一个元素
curValue
,(必须),当前遍历到的元素
curIndex
,当前遍历元素的索引
array
,数组引用
initialValue
,(可选),传递给回调函数的初始值
返回值: 回调函数遍历执行整个数组最后返回的结果。一个值
6.3 reduce边界处理 如果数组为空且未指定初始值 initialValue ,会抛出 TypeError
。
*如果数组仅有一个元素(无论位置如何)并且没有提供初始值 initialValue ,或者有提供 initialValue 但是数组为空,那么此唯一值将被返回且 callbackfn
不会被执行 *
1 2 3 4 5 6 7 [,,1 ].reduce ((x,y )=> { console .log ('调用回调函数' ) return x+y }) [].reduce (()=> {console .log ('调用回调函数' )},1 )
数组在遍历过程中发生变化的处理: (与map、filter的一致)
遍历过程中,新添加的元素不访问
遍历过程中,修改了已经被访问过的元素对最终返回结果没有影响
遍历过程中,修改或者删除即将访问(还未访问)的元素,被删除的元素访问不到,被修改的访问到的是修改后的元素值
6.4 手写模拟reduce 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 Array .prototype .myReduce = function (fn,initValue ){ if (typeof fn !== 'function' ) return var array = this if (array.length ==0 && initValue===undefined ) return if (array.length ==1 && initValue===undefined ) return array[0 ] if (array.length ==0 && initValue !== undefined ) return initValue array = array.filter (x => x!==null ) var pre = initValue || array[0 ] var i = initValue===undefined ?1 :0 for (;i<array.length ;i++){ pre = fn (pre,array[i],i,array) } return pre } [].myReduce (()=> {console .log ('调用回调函数' )},0 ) [2 ,3 ].myReduce ((x,y )=> x+y,1 ) [0 ].myReduce (()=> {}) [,,1 ].myReduce ((x,y )=> x+y,1 )
在模拟和测试过程中,又发现了reduce的一个处理特征。即:如果数组中含有null
、undefiined
,在reduce的回调函数处理时会跳过这些元素!
1 2 3 [,,1 ].reduce ((x,y )=> x+y,2 ) [undefined ,,1 ].reduce ((x,y )=> x+y,2 ) [,null ,1 ].reduce ((x,y )=> x+y,2 )
七、instanceof 严格来说,instanceof并不是函数,而是一个运算符
作用: 判断一个实例是否是某个构造函数的实例。instanceof在查找过程中会遍历左边变量的原型链,直到等于右边变量的prototype,若一直不等于,返回false
1 2 3 4 5 var arr = [] arr instanceof Array arr instanceof Function arr instanceof Object
下面是模拟实现:
1 2 3 4 5 6 7 8 9 10 11 12 const myInstanceof = (L,R )=>{ while (L!==null ){ if (L.__proto__ === R.prototype ) return true L = L.__proto__ } return false }myInstanceof ([],Array ) myInstanceof ([],Function ) myInstanceof ([],Object )
八、new **new
运算符 **创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
1 2 new constructor[(arguments )]
new 的过程:
创建一个空的对象 {}
将该对象的__proto__
属性指向构造函数的prototype
对象
将该对象作为构造函数执行时的this上下文对象
把构造函数执行一遍
如果构造函数没有返回值,返回this
1 2 3 4 5 6 7 new Foo ('name' )new Foo ()new Foo
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 var value = 1 function bar (name,age ){ this .name = name this .age = age value = 2 console .log (name) console .log (this .age ) console .log (value) console .log (this .value ) } bar.prototype .college = 'scnu' var obj = new bar ('timegogo' ,25 )console .log (obj) obj.college
模拟实现new
1 2 3 4 5 6 function myNew = function (fn,...args ){ const res = {} if (fn.prototype !==null ) res.__proto__ = fn.prototype const result = fn.apply (res,args) return result instanceof Object ? result :res }