前言
在之前的学习中,我们详细分析并模拟实现了 call , apply 方法,这一次,我么详细学习一下 JavaScript 里另外一个比较重要的东西,就是 bind
正文内容
先上定义,来认识一下 bind
MDN上是这样解释的: … bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。 …
乍一看,这又是一个操作 this 指向的方法,细品这个定义之后,我们可以看出虽然 bind 与 apply / call 一样都能改变函数this指向,但 bind 并不会立即执行函数,而是返回一个绑定了 this 的新函数,需要再次调用此函数才能达到最终执行。
按照惯例,在没掌握这个知识点之前,晦涩难懂的文档?狗都不看!(没有没有,狗头保命) 直接举例子!
var obj = {
z: 1
};
var obj1 = {
z: 2
};
function fn(x, y) {
console.log(x + y + this.z);
}
var bound = fn.bind(obj, 2);
bound(3);
bound.call(obj1, 3);
控制台输出了两个 6,我们就可以知道 bind 函数的特点
- bind 函数不会立刻执行,需要再一次调用
- 支持函数柯里化,在返回 bound 函数时已传递了一部分参数,在调用时 bound 补全剩余参数
- bind 会返回一个绑定了 this 的新函数 boundFcuntion,就是上面的 bound
- 这个返回的新函数不能再次修改指向,就算是 call 和 apply 也不行。
一开始看到 “函数柯里化” 这几个字我又懵了,原谅我是个纯小白… 稍微科普一下 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
所谓函数柯里化其实就是在函数调用时只传递一部分参数进行调用,函数会返回一个新函数去处理剩下的参数
来个例子
function add(a, b) {
return a + b;
}
function curry(fn, a) {
return function(b) {
return fn(a, b);
}
}
curry(add, 10)(1);
嗷嗷明白了,柯里化存在的意义也就是优化代码,比如我们进行累加,但是在复杂的应用时,总有持续变化的值,比如需要五个数相加,有四个值恒定,剩下的值需要其他地方毫无规律的产生,那这种情况下我们就可以考虑把前四个值的运算剥离出来,这样每一次传入第五个值时,只需要做一次运算,在高并发时减少的运算量可以节约大量的资源。
模拟实现
根据之前 call , apply 的经验,我们先来尝试一下。
尝试一
Function.prototype.my_bind= function (obj) {
var fn = this;
return function () {
fn.apply(obj);
};
};
var obj = {
z: 1
};
function fn() {
console.log(this.z);
};
var bound = fn.my_bind(obj);
bound();
打印一下可以输出 1,那就代表我们已经实现了 改变 this 的指向,接下来我们给他加上传参功能。
尝试二
我们在传参数时还应该实现函数柯里化
Function.prototype.my_bind = function (obj) {
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
return function () {
var params = Array.prototype.slice.call(arguments);
fn.apply(obj, args.concat(params));
};
};
var obj = {
z: 4
}
function fn(x, y, t) {
console.log(x + y + t + this.z);
}
var bound = fn.my_bind(obj, 1, 3);
bound(2);
执行流程也不是很难,var bound = fn.my_bind(obj, 1, 3); 将一个 this 指向的对象和两个实际参数以 arguments 的形式保存,然后切割出实际参加运算的参数保存到数组 args ,然后将整个方法作为结果返回给 bound ,然后执行 bound 进行计算。
这里捋一下指针的变化: 执行var bound = fn.my_bind(obj, 1, 3); 时:
fn.my_bind 的时候 this 指向 fn 函数var fn = this; 的时候 将 this 指向的 fn 函数保存到变量 fn 里
执行bound(2); 时:
fn.apply(obj, args.concat(params)); 的时候 fn 指向 fn 函数
结果是输出 5 ,实现了参数传递的功能,同时也支持函数柯西化,写这一段代码也学到了许多东西。
Array.prototype.slice.call(arguments, 1); 这一句的意思就是把调用方法的参数截取出来。比如这个例子
function test(a,b,c,d)
{
var arg = Array.prototype.slice.call(arguments,1);
alert(arg);
}
test("a","b","c","d");
我们没有直接采用arguments.slice(1) 的原因也很简单,就是因为 arguments 是一个类数组,并不是真正数组,并不能直接调用数组的处理方法。
Array.prototype.slice.call(arguments); 这一句的意思也就好理解了,实质上就是将类数组中的元素转化为数组(将函数的实际参数转换成数组)var fn = this 这一句也很关键,如果不提前保存 this 的指向,在执行 bound 的时候,就会指向window
到这里,我们基本上可以说已经实现了这个功能,但是似乎还是有些问题,因为 bind 还有一条比较少见的特性
绑定函数也可以使用 new 运算符构造,它会表现为目标函数已经被构建完毕。提供的 this 值会被忽略,但前置参数仍会提供给模拟函数。
这段话应该是说通过 bind 返回的 boundFunction 函数也能通过new运算符构造,只是在构造过程中,boundFunction 已经确定的 this 会被忽略,且返回的实例还是会继承构造函数的构造器属性与原型属性,并且能正常接收参数。 来个例子看看:
var z = 0;
var obj = {
z: 1
};
function fn(x, y) {
this.name = '姓名';
console.log(this.z);
console.log(x);
console.log(y);
}
fn.prototype.age = 26;
var bound = fn.bind(obj, 2);
var person = new bound(3);
console.log(person.name);
console.log(person.age);
在这里,我们首先通过fn.bind(obj, 2) 使得 this 指向了 obj,紧接着使用new操作符构造了bound函数,得到了实例person。通过结果输出,发现除了 this 原本指向的值找不到了,构造器的属性( name ),传入的参数( x 和 y ),原型属性( fn.prototype.age )都有顺利继承.
作为一名习惯 java 开发的人来看,new 操作符自然和类有关系,但是在 es6 以前应该是还没有类的概念的,构造函数其实只是对于类的模拟,这也就产生了一个问题,所有的构造函数除了可以使用 new 构造调用以外,它还能被普通调用,比如上面例子中的 bound 我们也可以普通调用:
var bound = fn.bind(obj, 2);
bound(2);
但是按照绑定类型来看,bound(2); 的 this 不应该是指向 window 吗,这里又有一点新的小知识: bind 属于显示绑定,bound(2); 部分其实本质是window.fn.bind(obj, 2); ,按照绑定规则来看,函数 fn 存在 this 默认绑定 window 与显示绑定 bind,而显示绑定优先级高于默认绑定,所以this还是指向obj。
具体的内容涉及到JavaScript里的各种绑定方式,但是介于篇幅太长在这里说可能会越学越歪,所以关于绑定方式详细内容可以看这一篇博文: 。。。(后续更新)。。。
好的咱们继续说回到 new ,那为什么 this 的指向会在 new 之后失效呢,当构造函数被 new 构造调用时,本质上构造函数中会创建一个实例对象,函数内部的 this 指向这个新创建的实例,当执行到console.log(this.z) 这一行时,this所指向的新示例上并未被赋予属性 z,所以输出 undefined ,这也解释了为什么bound函数被new构造时会丢失原本绑定的 this。
emm 明白是明白了,但是总感觉这样的代码虽然自由度高,但是也太混乱了,假如你和其他人合作写项目,在功能里你的 this 指向你想要的东西,结果他一个 new 传参,指向的对象都不一样了… 据说在 ES6 里增加了 class 的概念,就是为了解决这样的问题,使得构造函数只能通过 new 来调用,而不能直接调用了。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
};
say() {
console.log(this.name + " " + this.age);
};
}
const person = new Person('小A', 20);
person.say();
const person1 = Person();
随便找了个例子验证一下,发现确实会报错。
嗯,其实说了这些,无非就是让自己加深一下印象,在模拟 bind 方法时,返回的 bound 函数在调用时得考虑 new 调用与普通调用,毕竟两者this指向不同。
通过查阅资料我们知道,构造函数实例的 constructor 属性永远指向构造函数本身。
function Fn(){};
var o = new Fn();
console.log(o.constructor === Fn);
而构造函数在运行时,函数内部this指向实例,所以this的constructor也指向构造函数:
function Fn() {
console.log(this.constructor === Fn);
};
var o = new Fn();
尝试三
所以我们的改进思路就来了,就用 constructor 属性来判断当前bound方法调用方式,毕竟只要是new 调用,this.constructor === Fn 一定为 true。
Function.prototype.my_bind = function (obj) {
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
var bound = function () {
var params = Array.prototype.slice.call(arguments);
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
};
bound.prototype = fn.prototype;
return bound;
};
var obj = {
z: 4
}
function fn(x, y, t) {
console.log(x + y + t + this.z);
}
var bound = fn.my_bind(obj, 1, 3);
bound(2);
这一次,我们在使用 apply 方法时,进行了判断,分别根据不同的情况做出选择。
但是一旦涉及到原型,原型链就还需要考虑一些问题,虽然构造函数产生的实例都是独立的存在,实例继承而来的构造器属性随便你怎么修改都不会影响构造函数本身,但是如果我们直接修改实例原型,这就会对构造函数 Fn 产生影响。
function Fn() {
this.name = '小A';
};
var o = new Fn();
o.name = 'echo';
var o1 = new Fn();
console.log(o1.name)
这段代码很好理解,创建出来示例之后对实例的值任意修改,都不会影响下一个实例的构造。 但是如果这样修改
function Fn() {
}
var o = new Fn();
o.__proto__.name = "小B"
var o1 = new Fn();
console.log(o1.name)
修改以后 new 出来的都会是改变后的值,这个现象理解起来也不难。
构造器属性(this.name)在创建实例时,我们可以抽象的理解成实例拷贝了一份,这是属于实例自身的属性,后面再改都与构造函数不相关。而实例要用 prototype 属性时都是顺着原型链往上找,构造函数有的话就给实例用了,一共就这一份,谁要是改了那就都得变。
那我么在试试修改一下这个 bind
尝试四
所以我们的思路其实已经有了,只需要搞出来一份构造函数的副本,把这个副本当作构造函数来用,每一次修改都修改副本的值,这样想要恢复起来也只需要用原本的构造函数覆盖掉副本。
Function.prototype.my_bind = function (obj) {
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
var bound = function () {
var params = Array.prototype.slice.call(arguments);
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
};
fn_.prototype = fn.prototype;
bound.prototype = new fn_();
return bound;
};
最后,bind 方法如果被非函数调用时会抛出错误,所以我们要在第一次执行 my_bind 时做一次调用判断,加个条件判断。 最终版本
Function.prototype.my_bind = function (obj) {
if (typeof this !== "function") {
throw new Error("非法调用");
}
var args = Array.prototype.slice.call(arguments, 1);
var fn = this;
var fn_ = function () {
}
var bound = function () {
var params = Array.prototype.slice.call(arguments);
fn.apply(this.constructor === fn ? this : obj, args.concat(params));
};
fn_.prototype = fn.prototype;
bound.prototype = new fn_();
return bound;
};
var obj = {
z: 4
}
function fn(x, y, t) {
console.log(x + y + t + this.z);
}
var bound = fn.my_bind(obj, 1, 3);
bound(2);
到这里终于是一步步走过来了,考虑的已经比较全面了。
总结
模拟 bind 的过程花了好久,尽管有很多大佬已经写好的资料可以参考,但是一人一个思路,虽然有共同点,但是混杂在一起并不好理解,只能看懂之后自己重新串一下。可能串完还是不够熟悉,不过多看几遍应该就好了,hhh
|