关于手写bind()与new操作符的思考
call(),apply(),bind()的异同
相同
改变函数运行时的this
指向,好处是将任意对象设置为任意函数的作用域,这样对象可以不用关心方法
传入的第一个参数如果为null
或undefined
,则默认全局对象,在浏览器环境下是window
对象,在node.js
环境下是global
对象(所以在手写时用globalThis指代全局对象,只写window是不够严谨的)
不同
传入的第二个参数:apply()传入数组或类数组,call()和bind()传入参数列表
参数传递次数:apply()和call()都是一次性传入参数,而bind()可以分多次传入
参数返回结果:apply()和call()都是立即执行函数,bind()返回绑定this对象后的函数
手写bind()
var a = 3, b = 4; // 必须用var声明才会存储在变量对象中
// 定义函数
function sum(m, n){
return this.a + this.b + m + n;
}
// 定义对象
let obj = {
a : 1,
b : 2
}
// 将obj的作用域绑定到sum函数中返回一个新函数newsum
let newSum = sum.bind(obj);
// 用新函数newsum构造函数创建实例对象o,newsum是函数,所以实例对象o也是函数
let o = new newSum(7,8);
// 不使用bind()的输出
console.log(sum(5,6)); // 18
console.log(newSum(5,6)); // 14
console.log(o); // sum{}
思路
-
bind()是函数原型上的方法,所有函数都可以使用,所以写在函数原型上
// thisValue为传入的第一个参数,要绑定的对象 Funtion.prototype.Mybind = function(thisValue){}
-
参数不合理处理:thisValue必须是引用类型,当其为null或undefined时,thisVal=globalThis,否则应该将原始类型转为引用类型
Funtion.prototype.Mybind = function(thisValue){ thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue); }
-
由于bind()传入的是参数列表,所以使用
arguments
对象获取传入的参数更方便,由于bind()是可以分多次传参,所以要分别获取两次的参数列表后再进行拼接得到最终的参数列表,可以使用数组的slice()
截取参数,concat()
拼接参数 -
在bind()方法中,this对象是指调用bind()方法的函数,在上面的代码中sum函数调用bind()方法,所以在bind()方法中this指代sum函数
Funtion.prototype.Mybind = function(thisValue){ thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue); let fn = this; // 第一次传参 // arguments是类数组,没有slice()方法,所以要在slice()方法上使用call()使用arguments的作用域截取 let args1 = Array.prototype.slice.call(arguments, 1); }
-
bind()返回的是函数,这个函数是绑定thisValue作用域的函数,那么如何在原来的函数绑定新的作用域呢?只要我们对原来的函数使用apply()方法就可以实现在任意函数使用任意对象的作用域了
Funtion.prototype.Mybind = function(thisValue){ thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue); let fn = this; let args1 = Array.prototype.slice.call(arguments, 1); // bind()方法返回函数,这个函数是使用了thisValue作用域的fn函数 return function(){ //这里写的内容不属于bind()方法了 // 第二次传参 let args2 = Array.prototype.slice.call(arguments); // 两次传参拼起来 let args = args1.concat(args2); // 返回使用thisValue作用域的fn函数的返回值 return fn.apply(thisValue, args); } }
-
将bind()返回的函数理解成构造函数(构造函数就是一个函数),就可以使用new操作符创建构造函数的实例对象
-
在构造函数(bind()返回的函数)中,this指代的是构造函数创建的实例对象
Funtion.prototype.Mybind = function(thisValue){ thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue); let fn = this; let args1 = Array.prototype.slice.call(arguments, 1); return function constr(){ // 给构造函数取名为constr,这里是constr的内容,与bind()无关 let args2 = Array.prototype.slice.call(arguments); let args = args1.concat(args2); // 构造函数创建的实例对象 let obj = this; return fn.apply(thisValue, args); } }
-
new操作符在创建的过程中,将实例对象与构造函数的原型对象通过原型链连接起来,要理解这一步如何判断,首先要理解new在创建实例对象的过程,先来看手写new的代码
function myNew(fn, ...args) { if (typeof (fn) !== 'function') { throw new Error('current params is not correct!'); } let obj = {}; // 1.创建一个新对象 obj.__proto__ = fn.prototype; // 2.将新对象与构造函数通过原型链连接 let result = fn.apply(obj, args); // 3.将构造函数中的this绑定到新对象上 // 4.执行构造函数内部的代码,通过apply调用构造函数 return result instanceof Object ? result : obj; // 5.如果构造函数返回非空对象则返回该对象,否则返回新对象 }
将新函数newSum和创建的实例对象o结合这段手写new代码看一下
首先传入的fn是newSum,新创建一个obj对象,将obj与newSum通过原型链连接,让构造函数newSum绑定obj的作用域,输出newSum函数在obj的作用域上的运行结果let newSum = sum.bind(obj); let o = new newSum(7,8);
newSum
也就是我们手写bind()中返回的构造函数constr
,我们对constr使用new创建实例对象时,会先创建一个新对象,然后将新对象与constr通过原型链连接,然后返回constr.apply()
的运行结果
那也就是如果我们对bind()返回的函数使用了new,实例对象的__proto__属性就会等于构造函数constr的prototypeFuntion.prototype.Mybind = function(thisValue){ thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue); let fn = this; let args1 = Array.prototype.slice.call(arguments, 1); return function constr(){ let args2 = Array.prototype.slice.call(arguments); let args = args1.concat(args2); let obj = this; // 判断是否使用了new操作符 if (obj.__proto__ === constr.prototype) { // 创建obj } // 没有使用 else return fn.apply(thisValue, args); } }
-
那么new操作符是如何创建实例对象的呢?这里我们需要知道,我们在bind()返回的函数
constr
中做的实际上就是让原来的函数fn
使用thisValue
的作用域运行,所以在创建实例对象这里我们实际上就是创建原来的函数fn的实例对象Funtion.prototype.Mybind = function(thisValue){ thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue); let fn = this; let args1 = Array.prototype.slice.call(arguments, 1); return function constr(){ let args2 = Array.prototype.slice.call(arguments); let args = args1.concat(args2); let obj = this; if (obj.__proto__ === constr.prototype) { return new fn(...args); } else return fn.apply(thisValue, args); } }
到这里就清晰了,我们在使用new操作符时是将实例对象与构造函数通过原型链连接,而为在创建实例对象时,实际上是在创建原来函数的实例对象,不需要使用到构造函数(构造函数起到一个判断是否使用了new操作符的作用)
手写bind()
Function.prototype.Mybind = function (thisValue) {
thisValue = thisValue === null || thisValue === undefined ? globalThis : Object(thisValue);
let fn = this;
let args1 = Array.prototype.slice.call(arguments, 1);
return function constr() {
let args2 = Array.prototype.slice.call(arguments);
let args = args1.concat(args2);
if (this.__proto__ === constr.prototype) {
return new fn(...args);
// new创建过程,手写代码
// let obj = {};
// obj.__proto__ = fn.prototype;
// let res = fn.apply(obj, args);
// return res instanceof Object ? res : obj;
}
else return fn.apply(thisValue, args);
}
}