1.手写call
Function实例的call()方法会以给定的this值和逐个提供的参数调用该函数。
我们先看看call的使用和作用
let foo = {
value: 1
}
function bar() {
console.log(this.value);
}
bar.call(foo) // 1
有两点:
- call改变了this的指向,指向到foo
- bar函数执行了
1.1 第一版
let foo = {
value: 1,
bar: function () {
console.log(this.value)
}
};
foo.bar(); // 1
这是我们模拟的一个方法,这个时候this就指向foo,我们再用delete删除foo新增的bar属性即可。
拆解步骤为:
- 将函数设为对象的属性;
- 执行函数;
- 删除该函数;
以代码形式展示就是
foo.fn = bar
foo.fn()
delete foo.fn
根据以上思路,写出第一版:
// 第一版
Function.prototype.myCall1 = function (ctx) {
ctx.fn = this
ctx.fn()
delete ctx.fn
}
let foo = {
value: 1
}
function bar() {
console.log(this.value);
}
bar.myCall1(foo) // 1
1.2 第二版
call除了指定this,还可以给定参数
let foo = {
value: 1
}
function bar(a,b) {
console.log(this.value);
console.log(a);
console.log(b);
}
bar.call(foo,'aaa','bbb')
// 1
// aaa
// bbb
那第二版我们就可以使用剩余参数语法,将除了第一个参数的其余参数,以数组形式拿到
// 第二版
Function.prototype.myCall2 = function (ctx, ...rest) {
ctx.fn = this
ctx.fn(...rest)
delete ctx.fn
}
let foo = {
value: 1
}
function bar(a,b) {
console.log(this.value);
console.log(a);
console.log(b);
}
bar.myCall2(foo,'aaa','bbb')
// 1
// aaa
// bbb
1.3 第三版
this参数有可能为null,undefined,甚至于不是Object,那这种时候,视为指向全局的this
var value = 1
function bar() {
console.log(this.value)
}
bar.call(null) // 1
并且函数是可以实现返回值的
let foo = {
value: 1
}
function bar(a, b) {
console.log(this.value); // 1
return a + b
}
console.log(bar.call(foo, 2, 3)); // 5
根据这两个要求,写出第三版
// 第三版
Function.prototype.myCall3 = function (ctx, ...rest) {
ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
ctx.fn = this
const result = ctx.fn(...rest)
delete ctx.fn
return result
}
let foo = {
value: 1
}
function bar(a, b) {
console.log(this.value); // 1
return a + b
}
console.log(bar.myCall3(foo, 2, 3)); // 5
1.4 最终版
根据第三版进行优化,使用symbol确保key的唯一性
// 第四版
Function.prototype.myCall = function (ctx, ...rest) {
ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
const key = Symbol("temp")
ctx[key] = this
const result = ctx[key](...rest)
delete ctx[key]
return result
}
let foo = {
value: 1
}
function bar(a, b) {
console.log(this.value); // 1
return a + b
}
console.log(bar.myCall(foo, 2, 3)); // 5
2. 手写apply
Function 实例的 apply() 方法会以给定的 this 值和作为数组(或类数组对象)提供的 arguments 调用该函数。
apply和call近乎一样,我们可以以call的最终版进行修改
// apply
Function.prototype.myApply = function (ctx, arr) {
if (arr !== null && arr !== undefined && typeof arr[Symbol.iterator] !== 'function') {
throw new Error('第二个参数必须为iterator对象')
}
ctx = (ctx === null || ctx === undefined) ? globalThis : Object(ctx)
const key = Symbol("temp")
ctx[key] = this
const result = arr ? ctx[key](...arr) : ctx[key]()
delete ctx[key]
return result
}
var value = 0
let foo = {
value: 1
}
function bar(a, b) {
console.log(this.value); // 1
return a + b
}
console.log(bar.myApply(foo, [2, 3])); // 5
3.手写bind
Function 实例的 bind() 方法创建一个新函数,当调用该新函数时,它会调用原始函数并将其 this 关键字设置为给定的值,同时,还可以传入一系列指定的参数,这些参数会插入到调用新函数时传入的参数的前面。
var foo = {
value: 1
};
function bar() {
console.log(this.value);
}
// 返回了⼀个函数
var bindFoo = bar.bind(foo);
bindFoo(); // 1
可以看出bind有两个特点:
- 返回一个函数
- 可以传入参数
3.1 第一版
var value = 2
var foo = {
value: 1
}
function bar(a, b) {
this.habit = 'shopping'
console.log(this.value);
console.log(a);
console.log(b);
}
// 第一版
Function.prototype.myBind1 = function (ctx) {
const fn = this
return function () {
return fn.apply(ctx)
}
}
const bindBar = bar.myBind1(foo)
bindBar()
3.2 第二版
接下来,关于参数的传递
var foo = {
value: 1
};
function bar(name, age) {
console.log(this.value);
console.log(name);
console.log(age);
}
var bindFoo = bar.bind(foo, 'daisy');
bindFoo('18');
// 1
// daisy
// 18
换而言之,就是参数其实可以拼接起来的concat
那根据这个思路写出第二版的代码
var value = 2
var foo = {
value: 1
}
function bar(a, b) {
this.habit = 'shopping'
console.log(this.value);
console.log(a);
console.log(b);
}
// 第二版
Function.prototype.myBind2 = function (ctx) {
const fn = this
const args = Array.prototype.slice.call(arguments, 1)
return function () {
const bindArgs = Array.prototype.slice.call(arguments)
return fn.apply(ctx, args.concat(bindArgs))
}
}
const bindBar = bar.myBind2(foo, 12)
bindBar(13)
3.3 第三版
bind还有一个特点,当bind返回的函数作为构造函数的时候,bind时指定的this值就会失效,但传入的参数依然生效。
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
// daisy
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// kevin
尽管在全局和foo中都声明了value值,但还是打印undefined,说明绑定的this已经失效了。
通俗来讲,其实就是this的绑定失效,this指回构造函数的实例上
根据这个思路写出第三版
var value = 2
var foo = {
value: 1
}
function bar(a, b) {
this.habit = 'shopping'
console.log(this.value);
console.log(a);
console.log(b);
}
// 第三版
Function.prototype.myBind3 = function (ctx) {
const fn = this
const args = Array.prototype.slice.call(arguments, 1)
const fBound = function () {
const bindArgs = Array.prototype.slice.call(arguments)
return fn.apply(this instanceof fBound ? this : ctx, args.concat(bindArgs))
}
fBound.prototype = this.prototype
return fBound
}
const bindBar = bar.myBind3(foo, 22)
const binBarNew = new bindBar(33)
3.4 第四版
第三版已经很完整了,但是考虑到我们直接将 fBound.prototype = this.prototype,那我们直接修改fBound.prototype的时候,也会直接修改绑定函数的prototype。
所以我们的思路是通过一个空函数来进行中转。
// 第四版
Function.prototype.myBind4 = function (ctx) {
const fn = this
const args = Array.prototype.slice.call(arguments, 1)
const fNop = function () { }
const fBound = function () {
const bindArgs = Array.prototype.slice.call(arguments)
return fn.apply(this instanceof fNop ? this : ctx, args.concat(bindArgs))
}
// fBound.prototype = this.prototype
fNop.prototype = this.prototype
fBound.prototype = new fNop()
return fBound
}
3.5 最终版
该版主要是预处理一些错误提示;
- 调用bind的不是函数时,提示错误。
- 绑定的ctx为null或undefined的时候,this指向全局
// 最终版
// 防止调用该函数的this为非函数
Function.prototype.myBind = function (ctx) {
if (typeof this !== 'function') {
throw new Error("Function.prototype.myBind - what is trying to be bound is not callable")
}
if (ctx === undefined || ctx === null) ctx = globalThis
const fn = this
const args = Array.prototype.slice.call(arguments, 1)
const fNop = function () { }
const fBound = function () {
const bindArgs = Array.prototype.slice.call(arguments)
return fn.apply(this instanceof fNop ? this : ctx, args.concat(bindArgs))
}
// fBound.prototype = this.prototype
fNop.prototype = this.prototype
fBound.prototype = new fNop()
return fBound
}