JavaScript进阶(四)手写JS经典函数

本文最后更新于: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,...])
//fn表示一个具体的函数对象

参数:

  1. thisArg:(可选)需要指定的上下文对象,可选,不指定或者指定为null、undefined时,为全局对象
  2. 后续参数:(可选)从第二个参数起,作为调用函数的参数,数量不固定

返回值:函数fn的返回值

1.2 手写模拟call

模拟实现流程:

  1. 将函数设为thisArg对象的属性
  2. 执行该函数
  3. 删除该函数

需要考虑的两个问题:

  1. this参数可能为null,当this为null时,视为指向window
  2. 需要返回函数结果
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 //this在此处为调用myCall的对象,也就是函数function

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) //23 24
log.call(obj,24) //23 24

二、apply

apply()方法调用一个具有给定 this值的函数,以及以一个数组(或一个类数组对象)的形式提供的参数。

2.1 apply语法

1
2
3
4
5
fn.apply(thisArg, argsArray)
//thisArg:指定的上下文对象
//argsArray:参数数组

//返回值:fn函数的返回值

参数:

  1. thisArg:(可选)需要指定的上下文对象,可选,不指定或者指定为null、undefined时,为全局对象
  2. argsArray:(可选)参数数组,Array类型,数组内部的所有元素作为函数fn的调用参数

返回值:函数fn的返回值

2.2 手写模拟apply

模拟实现流程:(与call相同,区别在于参数的形式,call是单独的参数,apply是参数数组)

  1. 将函数设为对象的属性。通过调用thisArg对象来调用函数,从而实现this指向thisArg的目的
  2. 执行该函数
  3. 删除该函数

需要考虑的问题: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 //此处的this是调用myApply的对象,即fn函数
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]) //10 'Q' 24
log.apply(obj,['Q',24]) //10 '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,...])
//fn:调用bind的函数

参数:

  1. thisArg:(可选)函数运行时的this对象,当thisArg缺省或者为null、undefined时,函数运行时的this对象取决于新函数的this对象
  2. 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() // 81,this为调用方法的对象,这里是module

var retrieveX = module.getX
retrieveX() // 9,this为调用方法的对象,这里是window全局对象

var boundGetX = retrieveX.bind(module)
boundGetX() // 81
boundGetX = boundGetX.bind(module2)
boundGetX() // 81,bind只能绑定一次,第二次绑定的无效

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) //3,表示接收3个参数(有3个形参)
var f1 = fn.bind(null,1)
console.log(f1) //2,表示接受2个参数,已经有一个参数1被预置了
var f2 = f1.bind(null,2)
console.log(f2) //1,表示接受1个参数,已经有两个参数[1,2]被预置了
f2(3) //6 //调用函数,参数依次为[1,2]+3,即[1,2,3]

3.3 手写模拟bind

模拟实现

  1. 返回一个绑定了this的函数
  2. bind可以传入参数,返回的函数也可以传入参数
  3. 对于返回的函数,如果使用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
//实现第1、2点
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) //0,这里与JavaScript自带的bind的表现不同,应该是一些细节没有模拟到位
var f2 = f1.myBind(null,2)
console.log(f2) //0
f2(3) //6,输出的结果是对的,说明预置参数的功能实现了

//测试:绑定this
function log(){console.log(this.name)}
var name = 'window'
var obj = {name:'obj'}
var fn3 = log.myBind(obj)
fn3() //obj,输出的结果是对的,说明绑定this的功能实现了
var obj2 = {name:'obj2'}
var fn4 = fn3.bind(obj2)
fn4() //obj

接下来模拟实现 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');
//输出如下:
//undefined 原来把this绑定到foo上的,new之后失效了,所以console.log(this.value)为undefined
//daisy 这里输出的是调用bind时预置的函数参数。说明预置的函数参数,在函数作为构造函数时,依然起到效果
//18 这里输出的是new+构造函数时,传入的age参数
console.log(obj) //bar{habit:'shopping'}
console.log(obj.friend) //kevin

可以看到,绑定的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; //这里的this等于调用bind函数的那个函数f的函数体
var args = Array.prototype.slice.call(arguments,1);
var fNOP = function(){};
var fBound = function(){
var bindArgs = Array.prototype.slice.call(arguments);

//当作为构造函数时,this指向实例,将绑定函数的this指向该实例,可以让实例获得来自绑定函数的值
//以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性

//当作为普通函数时,this指向window,此时绑定函数的this指向context
return self.apply(this instanceof fNOP? this:context,args.concat(bindArgs));
}
fNOP.prototype = this.prototype;

//修改返回函数的prototype 为绑定函数的prototype,实例就可以继承函数原型中的值
fBound.prototype = new fNOP();
return fBound;
}
//这一块还没研究熟

四、filter

filter() 方法创建给定数组一部分的浅拷贝 (en-US),其包含通过所提供函数实现的测试的所有元素

4.1 filter语法

1
arr.filter(function(element,index,array){...},thisValue)

参数:

  1. function(element, index, array){},(必须提供),提供测试判断逻辑的函数,针对每个元素进行测试,测试结果为true则加入返回的新数组

    回调函数必须 return 布尔值

  2. 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' //给还未访问的元素添加字符串,发生的变化立即体现在遍历过程中。
//这个过程存在一个隐藏的安全漏洞,就是会不断在数组末尾添加新元素,导致死循环、内存崩溃
//得益于filter的边界处理,遍历过程中新添加的元素并不影响遍历。
return word.length < 6;
}); //['spray']

要同时实现这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) //[5,3]

五、map

map,创建一个新数组,这个新数组由原数组中的每个元素都调用一次提供的函数后的返回值组成。调用map函数不会对原数组产生影响,除非在回调函数的第三个参数array,即数组引用那里对数组进行修改。

5.1 map语法

1
arr.map(function(element,index,array){...},thisValue)

参数:

  1. function(element,index,array),(必须),数组中每个元素都会执行这个回调函数,并将函数返回值组成新数组

    回调函数必须return返回值!

  2. 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, 8, NaN, 64]
[1,2,,4].map(x=>x**3) //[1, 8, 空, 64],可以看到还是有一点细微的差别

六、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)
//输出结果如下:
//x: 0 , y: 1
//x: 1 , y: 2
//x: 3 , y: 3
//6,这是函数返回的结果,上面3行都是console.log()输出的内容

//换一种表达方式,执行过程为:
// 0 + 1 + 2 + 3 = 6
//即把上一次循环的返回值作为参数一,本次遍历的元素作为参数二,执行回调函数

6.2 reduce语法

1
arr.reduce(function(preValue,curValue,curIndex,array){}, initialValue)

参数:

  1. function(preValue, curValue, curIndex, array),回调函数,每个数组元素都会依次执行
    • preValue,(必须),上一次调用回调函数fn的返回值。在第一次调用时,如果指定了initialValue,为该值;如果没有指定,则为数组第一个元素
    • curValue,(必须),当前遍历到的元素
    • curIndex,当前遍历元素的索引
    • array,数组引用
  2. 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
})
//1

[].reduce(()=>{console.log('调用回调函数')},1) //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) //0
[2,3].myReduce((x,y)=>x+y,1) //6
[0].myReduce(()=>{}) //0
[,,1].myReduce((x,y)=>x+y,1) //2

在模拟和测试过程中,又发现了reduce的一个处理特征。即:如果数组中含有nullundefiined,在reduce的回调函数处理时会跳过这些元素!

1
2
3
[,,1].reduce((x,y)=>x+y,2)			//3
[undefined,,1].reduce((x,y)=>x+y,2) //NaN
[,null,1].reduce((x,y)=>x+y,2) //3

七、instanceof

严格来说,instanceof并不是函数,而是一个运算符

作用: 判断一个实例是否是某个构造函数的实例。instanceof在查找过程中会遍历左边变量的原型链,直到等于右边变量的prototype,若一直不等于,返回false

1
2
3
4
5
//instanceof用法
var arr = []
arr instanceof Array //true
arr instanceof Function //false
arr instanceof Object //true

下面是模拟实现:

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) //true
myInstanceof([],Function) //false
myInstanceof([],Object) //true

八、new

**new 运算符 **创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

1
2
//语法格式,下面的arguments可选,()也可选
new constructor[(arguments)]

new 的过程:

  1. 创建一个空的对象 {}
  2. 将该对象的__proto__属性指向构造函数的prototype对象
  3. 将该对象作为构造函数执行时的this上下文对象
  4. 把构造函数执行一遍
  5. 如果构造函数没有返回值,返回this
1
2
3
4
5
6
7
//示例:
new Foo('name')
new Foo()
new Foo //与上一行代码等价
// 1、一个继承自 Foo.prototype的新对象被创建
// 2、将this绑定到这个新对象
// 3、如果构造函数没有显式返回对象,那么返回这个新对象;如果显式返回对象,返回指定的对象
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

// 验证 new 之后 会执行整个(构造)函数
console.log(name)
console.log(this.age)
console.log(value)
console.log(this.value) // 判断new 构造函数时,构造函数的this指向
}
bar.prototype.college = 'scnu'

var obj = new bar('timegogo',25)
// 输出如下:(说明new的时候会执行构造函数,new执行构造函数时,this值指向创建的新对象)
// timegogo
// 25
// 2
// undefined

console.log(obj)
// bar {name: 'timegogo', age: 25}

obj.college
// scnu (说明构建的新对象的__proto__指向了构造函数的prototype

模拟实现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
}

JavaScript进阶(四)手写JS经典函数
http://timegogo.top/2023/02/10/JavaScript/JavaScript进阶(四)手写JS经典函数/
作者
丘智聪
发布于
2023年2月10日
更新于
2023年7月16日
许可协议