文章目录
1. call的实现
参考资料:js 实现call和apply方法,超详细思路分析 - 听风是风 - 博客园 (cnblogs.com)
MDN的解释:Function.prototype.call() - JavaScript | MDN (mozilla.org)
function.call(thisArg, arg1, arg2, ...)
thisArg:
可选的。在 function
函数运行时使用的 this
值。请注意,this
可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 null
或 undefined
时会自动替换为指向全局对象,原始值会被包装。
1.1 设计思路
我们上看看call实现的一般效果。
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
const obj = {
foo: "fujiaxu",
};
function fn(){
console.log(this.foo);
}
fn();// fjx
fn.call(obj);// fujiaxu
可以看到,call()
可以给函数指定一个执行的上下文环境,用来改变该函数this的指向值(当然对于箭头函数来说是没有用的)。其次,它还可以传递参数,形式是散列传递。
那么我们如何手动模拟来实现call()
呢?我们把代码换一个格式:
const obj = {
foo: "fujiaxu",
fn:function(){
console.log(this.foo);
}
};
obj.fn();// fujaixu
我们把函数放在对象里面,当做它一个方法,通过这样的方式,我们就可以达到call的效果了。那么我们开始动手来手动模拟实现call吧。
1.2 实现改变上下文
在JavaScript中call()
方法是在Function.prototype.call()
中,所以我们定义函数的时候,也必须放在同样的位置,这样就可以使得所有的函数都可以调用我们自己编写的call()
方法。
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(){
console.log(this.foo);
}
const obj = {
foo: "fujiaxu",
};
Function.prototype.call_ = function(context){
context.fn = this;// 指向调用的函数
context.fn();
delete context.fn;
}
fn.call_(obj);// fujiaxu
这样,我们就是实现的改变上下文的操作。
1.3 传递参数
一种简单的方法就是使用ES6的剩余参数和扩展参数的方法,代码如下:
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
console.log(this.foo);
console.log(a,b,c);
}
const obj = {
foo: "fujiaxu",
};
Function.prototype.call_ = function(context,...args){
context.fn = this;// 指向调用的函数
context.fn(...args);
delete context.fn;
}
fn.call_(obj,1,2,3);
没错,就是这么简单,但是面试官可不会放过我们。所以我们还是要学习不适用es6的方法。那就是使用arguments
。从下标1开始,都是我们传入的参数,我们应该要将1 ~ length-1
的数据提取出来,可惜的是arguments
只不过是伪数组,没有数组的方法,当然也不能使用Array.prototype.slice.call(arguments)
,我们可是在模拟call
啊,只能老老实实的使用循环来转换。另外一个问题,怎么把数据传递给函数呢?总不能直接传递数组啊,所以我们要将它转换为字符串。
数组转换为字符串[1,2,3] => "1,2,3"
,在加上eval()就可以实现了(为什么要使用eval,而不是直接传递转换好的字符串给函数呢?这是因为这个字符串知识相当于一个变量,而不是多个变量,所以要使用eval来传递多个参数)。当然还有一个问题,若["a","b","c"]=> "a,b,c"
,这在eval函数中是eval(“fn(a,b,c)”),这样话,肯定会报错的,我么在这里的需要的是fn(“a”,“b”,“c”),所以在循环压入数组的时候,压入的是"arguments[" + i + "]")
,而不是里面的数据,让eval
直接读取arguments
的数据。
代码如下:
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
console.log(this.foo);
console.log(a,b,c);
}
const obj = {
foo: "fujiaxu",
};
Function.prototype.call_ = function(context){
context.fn = this;// 指向调用的函数
let arr = [];
for(let i=1;i<arguments.length;i++){
arr.push("arguments["+i+"]");
}
eval("context.fn("+arr+")");
delete context.fn;
}
fn.call_(obj,"aw",2,3);
1.4 null和undefined处理,返回值的处理
对于函数返回值的处理,很简单,直接在最后返回就好了。
在非严格模式下,null和undefined 都指向的是window。
同时考虑到,传入的context非对象的情况下。
'use strict'
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
console.log(this.foo);
console.log(a,b,c);
return "hahah";
}
const obj = {
foo: "fujiaxu",
};
Function.prototype.call_ = function(context){
/* 判断是不是严格模式 */
let is = function(){
return this;
}()
// 对于严格模式的处理
if(is === window || (context !== undefined && context !== null)){
context = new Object(context) || window;
}else {
context = undefined;
}
console.log(context);
context.fn = this;// 指向调用的函数
let arr = [];
for(let i=1;i<arguments.length;i++){
arr.push("arguments["+i+"]");
}
let res = eval("context.fn("+arr+")");
delete context.fn;
return res;
}
fn.call_(obj,"aw",2,3);
以上就是call()
方法的实现过程。
2. apply的实现
在实现了call()
的基础上,只需要吧传递的参数改为数组就好,直接看代码:
'use strict'
var foo = 'fjx';// 这里必须使用var,如果使用let,那么它不会挂载到window 对象上
function fn(a,b,c){
console.log(this.foo);
console.log(a,b,c);
return "hahah";
}
const obj = {
foo: "fujiaxu",
};
Function.prototype.apply_ = function(context,array){
/* 判断是不是严格模式 */
let is = function(){
return this;
}()
if(is === window || (context !== undefined && context !== null)){
context = new Object(context) || window;
}else {
context = undefined;
}
let res;
let arr = [];
context.fn = this;// 指向调用的函数
if(!array){// 这里不能用长度判断,因为,不知道是否传入有参数,那么可能没有length这个属性
res = context.fn();
}else{
for(let i=0;i<array.length;i++){
arr.push("array["+i+"]");
}
res= eval("context.fn("+arr+")");
}
delete context.fn;
return res;
}
fn.apply_(obj,["aw",2,3]);
3. bind() 的实现
参考资料:
Function.prototype.bind() - JavaScript | MDN (mozilla.org)
js 手动实现bind方法,超详细思路分析! - 听风是风 - 博客园 (cnblogs.com)
JavaScript深入之bind的模拟实现 · Issue #12 · mqyqingfeng/Blog (github.com)
首先看一下bind()
有哪些特点
- 返回一个新的函数(this的值改变),且这个函数的this值不会再被改变(闭包的原理)
- 函数柯里化,也就是说可以先传递一部分参数
- 绑定函数自动适应于使用
new
操作符去构造一个由目标函数创建的新实例。当一个绑定函数是用来构建一个值的,原来提供的this
就会被忽略。并且会继承prototype上的值
3.1 改变this并返回函数
bind
的调用与call()、apply()
不同,它不是立即执行,而是返回一个函数。
所以我们可以使用apply()
来实现。
var age = 1;// 必须使用var,否则let无法挂载在window
var foo = {
age:2
}
var bar = {
age:3
}
function fn() {
console.log(this.age);
}
fn();//1
Function.prototype.bind_ = function (contenxt){
let fn = this; // 保存函数
return function(){
fn.apply(contenxt);
}
}
let fnn = fn.bind_(foo);
fnn();// 2
3.2 传参的实现
bind()
支持函数柯里化,所以要使用闭包来传递一部分参数,然后剩余的参数以后传递。
var age = 1;// 必须使用var,否则let无法挂载在window
var foo = {
age:2
}
var bar = {
age:3
}
function fn(a,b) {
console.log(this.age);
console.log(a,b);
}
fn();
Function.prototype.bind_ = function (contenxt){
let fn = this; // 保存函数
let args1 = [...arguments].splice(1);// 当然也可已使用Array.prototype.slice.call(arguments, 1);
return function(){
/* 不能在这里保存函数,这里的this会变成window */
let args2 = [...arguments];
fn.apply(contenxt,[...args1,...args2]);// 或者 args1.concat(args2);
}
}
let fnn = fn.bind_(foo,1);
fnn(2); // 1, 2 而且这里的this是不会指向window的,看看代码就知道了使用了闭包了
至此,已近实现了两个特性。还剩下最后一个bind出的函数可以作为构造函数使用。
而且从这里我们也可以看出,一旦使用bind
变了this的值,那么将不会再改变。所以fnn(2)
是没有指向window的。那么这是为什么呢?因为在调用fnn(2)
的时候,它其实是在之前已经定义好的contenxt
,这里就是用了闭包,所以this还不回被改变。
同理,若使用新的函数再去bind()
一次,也是不会改变this
的值的。那这又是为什么?请仔细看看代码,我们这里使用的apply()
实现的bind()
,也就是说,我们其实是在给定的context里面调用fn
函数,但是在第一次bind
中context已经确定了,所以不会再被改变了(同样是闭包的原理)。
3.3 构造函数效果的模拟实现
一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。且返回的实例还是会继承构造函数的构造器属性与原型属性,并且能正常接收参数。
那么这是什么意思呢?
var z = 1;
function foo(x,y) {
this.age = 12;
console.log(this.z);
console.log(x,y);
}
foo.prototype.hh = "hh";
foo(2,3);
let fooo = foo.bind(z);
let person = new fooo(2,3);
console.log(person.hh);
结果为:
可以看到,这里的z
居然是undefined,那么就说明绑定的this值失效了,而且person
继承了foo的原型。
这就是我们需要实现的效果,所以在apply()
的时候就需要判断这时的this
是否为对象实例的this
var z = 1;
function foo(x, y) {
this.age = 12;
console.log(this.z);
console.log(x, y);
}
foo.prototype.hh = "hh";
foo(2, 3);
Function.prototype.bind_ = function (contenxt) {
let fn = this; // 保存函数
let args1 = [...arguments].splice(1); // 当然也可已使用Array.prototype.slice.call(arguments, 1);
let bound = function () {
/* 不能在这里保存函数,这里的this会变成window */
let args2 = [...arguments];
fn.apply(this instanceof fn ? this : contenxt, [...args1, ...args2]); // 或者 args1.concat(args2);
};
bound.prototype = fn.prototype;
return bound;
};
let fooo = foo.bind_(z);
let person = new fooo(2, 3);
console.log(person.hh);
这里我们采用的instanceof
来判断是否是使用的new。
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
原理:
function myInstanceof(left, right) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
if(proto === null) return false;
if(proto === right.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
bound.prototype = fn.prototype;
这句代码继承了原函数的的属性和方法,并且可以让instanceof
来判断。所以this instanceof fn
也是可以的。
但是我们还存在一个问题,那就是当新的函数修改自己的prototype
时,会把之前的prototype
也修改了,所以我们需要做一些处理。
可以使用一个空函数的实例,当然,也可以直接使用当前函数的实例。
var z = 1;
function foo(x, y) {
this.age = 12;
console.log(this.z);
console.log(x, y);
}
foo.prototype.hh = "hh";
foo(2, 3);
Function.prototype.bind_ = function (contenxt) {
let fn = this; // 保存函数
let args1 = [...arguments].splice(1); // 当然也可已使用Array.prototype.slice.call(arguments, 1);
let fNOP = function(){};
let bound = function () {
/* 不能在这里保存函数,这里的this会变成window */
let args2 = [...arguments];
fn.apply(this instanceof fn ? this : contenxt, [...args1, ...args2]); // 或者 args1.concat(args2);
};
fNOP.prototype = fn.prototype;
bound.prototype = new fNOP();
return bound;
};
let fooo = foo.bind_(z);
let person = new fooo(2, 3);
console.log(person.hh);
这样就不会影响到原函数的prototype
了。
最后在做一些处理:
- 原函数有返回值
- 不是函数调用
3.4 最终代码
Function.prototype.bind_ = function (contenxt) {
if (typeof this !== "function") {
throw new Error(
"Function.prototype.bind - what is trying to be bound is not callable"
);
}
let fn = this;
let args1 = [...arguments].splice(1);
let fNOP = function () {};
let bound = function () {
let args2 = [...arguments];
return fn.apply(this instanceof fn ? this : contenxt, [...args1, ...args2]);
};
fNOP.prototype = fn.prototype;
bound.prototype = new fNOP();
return bound;
};