有空的时候都会看一下掘金社区,里面的文章都很棒,也学到了很多,其实我写博客主要是让自己记个笔记,俗话说好记性不如个烂笔头,有时候会忘记,所以遇到解决问题的方法我都会记载下来,都归于项目总结这一类了。慢慢我发现我写的东西还能帮助到更多的人,这更让我开心了,同时我也告诉自己要更加严谨了,可不能误人子弟。
好了,刚刚有感而发,说了那么多开场白,下面进入正题吧。
1.我们先看一下call函数实现的方法
Function.prototype.realizeCall = function (context) {
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
context = context || window
context.fn = this
let arg = [...arguments].slice(1)
let result = context.fn(...arg)
delete context.fn
return result
}
这就是call()方法实现的原理,不过这个写法中用到了es6中的一些语法。那让我们具体来分析一下吧。
首先我们先对这个函数做一下改造,加几个console.log(),打印出我们疑惑的地方。
Function.prototype.realizeCall = function (context) {
// 先判断一下this是不是一个函数,这个this下面会打印出来
if (typeof this !== 'function') {
throw new TypeError('not funciton')
}
console.log(1,this); // 1 看一下this打印出来的结果
console.log(2,context); // 2 同时打印出context,看一下context的值
console.log(3,arguments); // 3 看一下传入进来的参数
context = context || window
context.fn = this
console.log(4,context); // 4 这个时候我们看一下context的值并且和2进行对比一下
console.log(5,context.fn); // 5 看一下context.fn的值
console.log(6,this); // 6 打印出这个时候的this值和1进行对比一下
let arg = [...arguments].slice(1)
console.log(7,arguments); // 7 看一下传入进来的参数并且和3对比一下
console.log(8,arg); // 看一下截取之后的参数
let result = context.fn(...arg)
delete context.fn
return result
}
我们先打印这八个看一下,执行函数如下:
Math.max.call(null,1,2,3,4,5)
// 5
Math.max.realizeCall(null,1,2,3,4,5)
// 1 ƒ max() { [native code] } ===可以看到到这是this指向的是Math.math()
// 2 null ===这个时候context是传入的null
// 3 Arguments(6) [null, 1, 2, 3, 4, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ] ===这个时候arguments也是传入的参数
// 4 Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …} ===经过转化之后context变成了默认的window,这是因为我们传入的是null,所以context || window得到是window
// 5 ƒ max() { [native code] } === 把this的值赋值给我们自己定义的context.fn方法(其实这个时候this可以看做一个方法)
// 6 ƒ max() { [native code] } === 和1对比可以看到this的值没有变化
// 7 Arguments(6) [Window, 1, 2, 3, 4, 5, callee: ƒ, Symbol(Symbol.iterator): ƒ] ==== 和3对比我们发现arguments的值有变化,context的值由null变成了window,这是因为在方法内部context = context || window,所以context改变了
// 8 (5) [1, 2, 3, 4, 5] === 最后截取的值,也是我们的参数值
// 5
从以上两种执行的结果可以看到,结果都是一样的,说明我们封装的这个方法是有效的。
其实实现这种方法的本质是 => 将要改变this指向的方法挂到目标this上执行并返回
从上面封装的方法来说 => 也就是把this赋值给context.fn 然后截取相应的参数,放在context.fn方法里执行,在把执行后的结果返回,而且把comtext.fn这个方法再删除掉,这就是call()方法执行的原理。
2、我们再看一下apply方法的实现原理吧
其实实现方法和call类似,只是call和apply传入参数的方式不一样,所以实现原理也就是处理参数的方式不一样。
我们先来看一下call和apply的区别:
Math.max.call(null,1,2,3,4,5,6,7,8,9)
// 9
let arr = [1,2,3,4,5,6,7,8,9]
Math.max.apply(null,arr)
// 9
可以看到传入的参数方式不一样,那让我们来看下apply的实现原理吧
Function.prototype.realizeApply = function(context){
if(typeof this !='function'){
throw new TypeError('not function')
}
context = context || window;
context.fn = this;
let result;
if(arguments[1]){
result = context.fn(...arguments[1])
}else{
result = context.fn()
}
delete context.fn;
return result;
}
let arr = [9,8,8,7,4,2,16,5]
Math.max.realizeApply(null,arr); // 16
Math.min.realizeApply(null,arr); // 2
可以看到apply的实现原理和call的实现原理很相似,只是因为传入参数方式不一样而已。
3、bind方法实现的原理
Function.prototype.realizeBind = function (context) {
if(typeof this !='function'){
throw new TypeError('not function')
}
context = context || window
context.fn = this;
let args = [...arguments].slice(1)
let fn = function () {
let fnArgs = [...arguments] // 新:参数
// bind 函数的参数 + 延迟函数的参数
// 用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上
context.fn.apply(this instanceof context.fn ? this : context, args.concat(fnArgs))
}
fn.prototype = Object.create(context.fn.prototype) // 维护原型
delete context.fn;
return fn
}
最后非常感谢这篇文章的启发:20道JS原理题助你面试一臂之力!