js 手动实现bind方法,超详细思路分析!

壹 ? 引

js 实现call和apply方法 一文中,我们详细分析并模拟实现了call/apply方法,由于篇幅问题,关于bind方法实现只能另起一篇。

在模拟bind之前,我们先了解bind的概念,这里引入MDN解释:

bind() 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用。

说的通俗一点,bindapply/call一样都能改变函数this指向,但bind并不会立即执行函数,而是返回一个绑定了this的新函数,你需要再次调用此函数才能达到最终执行。

我们来看一个简单的例子:

var obj = {
    z: 1
};
var obj1 = {
    z: 2
};

function fn(x, y) {
    console.log(x + y + this.z);
};
// call与apply
fn.call(obj, 2, 3); //6
fn.apply(obj, [2, 3]); //6

var bound = fn.bind(obj, 2);
bound(3); //6
//尝试修改bind返回函数的this
bound.call(obj1, 3); //6

可以到bind并不是立即执行,而是返回一个新函数,且新函数的this无法再次被修改,我们总结bind的特点:

  • 可以修改函数this指向。
  • bind返回一个绑定了this的新函数`boundFcuntion,例子中我们用bound表示。
  • 支持函数柯里化,我们在返回bound函数时已传递了部分参数2,在调用时bound补全了剩余参数。
  • boundFunction的this无法再被修改,使用call、apply也不行。

考虑到有的同学对于柯里化的陌生,所谓函数柯里化其实就是在函数调用时只传递一部分参数进行调用,函数会返回一个新函数去处理剩下的参数,一个经典简单的例子:

//函数柯里化
function fn(x, y) {
    return function (y) {
        console.log(x + y);
    };
};
var fn_ = fn(1);
fn_(1); //2

fn(1)(1) //2

不难发现函数柯里化使用了闭包,在执行内层函数时,它使用了外层函数的局部形参x,从而构成了闭包,扯远了点。

我们来尝试实现bind方法,先从简单的改变this和返回函数开始。

贰 ? 实现bind

之前已经有了模拟call/apply的经验,这里直接给出版本一:

Function.prototype.bind_ = function (obj) {
    var fn = this;
    return function () {
        fn.apply(obj);
    };
};

var obj = {
    z: 1
};

function fn() {
    console.log(this.z);
};

var bound = fn.bind_(obj);
bound(); //1

唯一需要留意的就是var fn = this这一行,如果不提前保存,在执行bound时内部this会指向window。

版本一以满足了this修改与函数返回,马上有同学就想到了,版本一不支持函数传参,那么我们进行简单修改让其支持传参:

Function.prototype.bind_ = function (obj) {
    //第0位是this,所以得从第一位开始裁剪
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    return function () {
        fn.apply(obj, args);
    };
};

完美了吗?并不完美,别忘了我们前面说bind支持函数柯里化,在调用bind时可以先传递部分参数,在调用返回的bound时可以补全剩余参数,所以还得进一步处理,来看看bind_第二版:

Function.prototype.bind_ = function (obj) {
    //第0位是this,所以得从第一位开始裁剪
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    return function () {
        //二次调用我们也抓取arguments对象
        var params = Array.prototype.slice.call(arguments);
        //注意concat的顺序
        fn.apply(obj, args.concat(params));
    };
};

var obj = {
    z: 1
};

function fn(x, y) {
    console.log(x + y + this.z);
};

var bound = fn.bind_(obj, 1);
bound(2); //4

看,改变this,返回函数,函数柯里化均已实现。这段代码需要注意的是args.concat(params)的顺序,args在前,因为只有这样才能让先传递的参数和fn的形参按顺序对应。

至少走到这一步都挺顺序,需要注意的是,bind方法还有一个少见的特性,这里引用MDN的描述

绑定函数也可以使用 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);//undefined 2 3

console.log(person.name);//听风是风
console.log(person.age);//26

在此例子中,我们先是将函数fnthis指向了对象obj,从而得到了bound函数。紧接着使用new操作符构造了bound函数,得到了实例person。不难发现,除了先前绑定好的this丢失了(后面会解释原因),构造器属性this.name,以及原型属性fn.prototype.age都有顺利继承,除此之外,两个形参也成功传递进了函数。

难点来了,至少在ES6之前,JavaScript并没有class类的概念,所谓构造函数其实只是对于类的模拟;而这就造成了一个问题,所有的构造函数除了可以使用new构造调用以外,它还能被普通调用,比如上面例子中的bound我们也可以普通调用:

bound(3); //1 2 3

有同学在这可能就有疑惑,bound()等同于window.bound(),此时this不是应该指向window从而输出0吗?我们在前面说bind属于硬绑定,一次绑定终生受益,上面的调用本质上等同于:

window.fn.bind(obj, 2);

函数fn存在this默认绑定window与显示绑定bind,而显示绑定优先级高于默认绑定,所以this还是指向obj

当构造函数被new构造调用时,本质上构造函数中会创建一个实例对象,函数内部的this指向此实例,当执行到console.log(this.z)这一行时,this上并未被赋予属性z,所以输出undefined,这也解释了为什么bound函数被new构造时会丢失原本绑定的this。

是不是觉得ES5构造函数特别混乱,不同调用方式函数内部this指向还不同,也正因如此在ES6中隆重推出了class类,凡是通过class创建的类均只能使用new调用,普通调用一律报错处理:

class Fn {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    };
    sayName() {
        console.log(this.name);
    };
};
//只能new构造调用
const person = new Fn('听风是风', 26);
person.sayName(); //听风是风
const person1 = Fn(); //Class constructor Fn cannot be invoked without 'new'

扯远了,让我们回到上面的例子,说了这么多无非是为了强调一点,我们在模拟bind方法时,返回的bound函数在调用时得考虑new调用与普通调用,毕竟两者this指向不同。

再说直白一点,如果是new调用,bound函数中的this指向实例自身,而如果是普通调用this指向obj,怎么区分呢?

不难,我们知道(强行让你们知道)构造函数实例的constructor属性永远指向构造函数本身,比如:

function Fn(){};
var o = new Fn();
console.log(o.constructor === Fn);//true

而构造函数在运行时,函数内部this指向实例,所以this的constructor也指向构造函数:

function Fn() {
    console.log(this.constructor === Fn); //true
};
var o = new Fn();
console.log(o.constructor === Fn); //true

所以我就用constructor属性来判断当前bound方法调用方式,毕竟只要是new调用,this.this.constructor === Fn一定为true。

让我们简单改写bind_方法,为bound方法新增this判断以及原型继承:

Function.prototype.bind_ = function (obj) {
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //通过constructor判断调用方式,为true this指向实例,否则为obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
    };
    //原型链继承
    bound.prototype = fn.prototype;
    return bound;
};

有同学就问了,难道不应该是this.constructor===bound吗?并不是,虽然new的是bound方法,本质上执行的还是fn,毕竟bound自身并没有构造器属性,这点关系还是需要理清。

其次还有个缺陷。虽然构造函数产生的实例都是独立的存在,实例继承而来的构造器属性随便你怎么修改都不会影响构造函数本身:

function Fn() {
    this.name = '听风是风';
    this.sayAge = function () {
        console.log(this.age);
    };
};
Fn.prototype.age = 26;

var o = new Fn();
o.sayAge(); //26
//我们改变实例继承的构造器属性,并不会影响构造函数本身
o.name = 'echo';
var o1 = new Fn();
console.log(o1.name) //听风是风

但是如果我们直接修改实例原型,这就会对构造函数Fn产生影响,来看个例子:

function Fn() {
    this.name = '听风是风';
    this.sayAge = function () {
        console.log(this.age);
    };
};
Fn.prototype.age = 26;

var o = new Fn();
o.sayAge(); //26
//修改实例的原型属性,这会影响构造函数本身
o.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age) //18

不难理解,构造器属性(this.name,this.sayAge)在创建实例时,我们可以抽象的理解成实例深拷贝了一份,这是属于实例自身的属性,后面再改都与构造函数不相关。而实例要用prototype属性时都是顺着原型链往上找,构造函数有便借给实例用了,一共就这一份,谁要是改了那就都得变。

我们可以输出实例o,观察它的属性,可以看到age属性确实是绑原型__proto__上(注意,prototype是函数特有,普通对象只有__proto__,都是原型,本质相同)。

怎么做才保险呢,这里就可以借助一个空白函数作为中介,直接看个例子:

function Fn() {
    this.name = '听风是风';
    this.sayAge = function () {
        console.log(this.age);
    };
};
Fn.prototype.age = 26;
// 创建一个空白函数Fn1,单纯的拷贝Fn的prototype
var Fn1 = function () {};
Fn1.prototype = Fn.prototype;
// 这里的Fn2对应我们的bound方法,将其原型指向Fn1创建的实例
var Fn2 = function () {};
Fn2.prototype = new Fn1();
var o = new Fn2();
console.log(o.age); //26
//尝试修改
o.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age);//26

说到底,我们就是借用空白函数,让Fn2的实例多了一层__proto__,达到修改原型不会影响Fn原型的目的,当然你如果通过无法直接通过__proto__.__proto__还是一样能修改,差不多就是这个意思:

o.__proto__.__proto__.age = 18;
var o1 = new Fn();
console.log(o1.age);//18

所以综上,我们再次修改bind_方法,拿出第四版:

Function.prototype.bind_ = function (obj) {
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    //创建中介函数
    var fn_ = function () {};
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //通过constructor判断调用方式,为true this指向实例,否则为obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
        console.log(this);
    };
    fn_.prototype = fn.prototype;
    bound.prototype = new fn_();
    return bound;
};

最后,bind方法如果被非函数调用时会抛出错误,所以我们要在第一次执行bind_时做一次调用判断,加个条件判断,我们来一个完整的最终版:

Function.prototype.bind_ = function (obj) {
    if (typeof this !== "function") {
        throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    };
    var args = Array.prototype.slice.call(arguments, 1);
    var fn = this;
    //创建中介函数
    var fn_ = function () {};
    var bound = function () {
        var params = Array.prototype.slice.call(arguments);
        //通过constructor判断调用方式,为true this指向实例,否则为obj
        fn.apply(this.constructor === fn ? this : obj, args.concat(params));
        console.log(this);
    };
    fn_.prototype = fn.prototype;
    bound.prototype = new fn_();
    return bound;
};

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); //undefined 2 3

console.log(person.name); //听风是风
console.log(person.age); //26
person.__proto__.age = 18;
var person = new fn();
console.log(person.age); //26

看着有些长,不过我们顺着思路一步步走过来其实不难理解。

好啦,关于bind方法的模拟实现就说到这里了,万万没想到这篇实现居然用了我五个小时时间...

另外,如果大家对于new一个构造函数发生了什么存在疑惑,可以阅读博主这篇文章:

new一个对象的过程,实现一个简单的new方法

若对于上文中修改实例原型会影响原构造函数存在疑虑,可以阅读博主这篇文章:

精读JavaScript模式(八),JS类式继承

那么就写到这里了,巨累,准备睡觉。

叁 ? 参考

深度解析bind原理、使用场景及模拟实现

MDN Function.prototype.bind()

最详尽的 JS 原型与原型链终极详解,没有「可能是」。(一)

JavaScript深入之bind的模拟实现

原文地址:https://www.cnblogs.com/echolun/p/12178655.html

时间: 2024-11-03 11:05:43

js 手动实现bind方法,超详细思路分析!的相关文章

Windows 2016 无域故障转移群集部署方法 超详细图文教程

转自:https://blog.csdn.net/demonson/article/details/81708809 Windows 2016 无域故障转移群集部署方法 超详细图文教程 故障转移群集是一个很实用的功能,而windows在2016版本开始,终于支持不用域做故障转移群集.在群集中,我们可以设定一个"群集IP"而客户端只需要根据这个"群集IP"就能连接当前群集的主服务器.而不必关心群集服务器之间的替换.而更棒的是,它是"去中心"的,它没

八皇后问题 递归实现 C语言 超详细 思路 基础

八皇后问题 :假设 將八个皇后放到国际象棋盘上,使其两两之间无法相互攻击.共有几种摆法? 基础知识: 国际象棋里,棋盘为8X8格. 皇后每步可以沿直线.斜线 走任意格. 思路: 1.想把8个皇后放进去,肯定最终每行只有一个皇后,每列只有一个皇后. 2.设个二维数组chess [ i ] [ j ] 模拟棋盘,cas存放摆法.i j 是表示i行j列: 写一个用于递归的函数,思路如下 3.从上往下一行行的放皇后,放下一行时从最左边(第0列)放起,如果不能放就往右挪一格再试.注意判断右边有没有越界出棋

.netcore 3.1高性能微服务架构:封装调用外部服务的接口方法--HttpClient客户端思路分析

众所周知,微服务架构是由一众微服务组成,项目中调用其他微服务接口更是常见的操作.为了便于调用外部接口,我们的常用思路一般都是封装一个外部接口的客户端,使用时候直接调用相应的方法.webservice或WCF的做法就是引用服务,自动生成客户端.在webapi2.0里,我们都会手动封装一个静态类.那么在.netcore3.1的微服务时代,我们该如何处理这个问题呢? ----思路都是一样的,封装一个外部服务,并且使用依赖注入和 HttpFactory工厂等.netcore特有的方式提升性能.接下来我们

awk学习心得--超详细--案例分析

awk:报告生成器格式化后显示信息 语法:awk [options] 'script' file1 file2, ...awk [options] 'PARTTERN { action }' file1 file2, ...最常见的action:print,printf awk的基本特征:a.每一次取一行b.根据指定的分隔符(不指定是位空白字符)将该行切割位列,使用$0(整行),$1,$2,$3...(第一列,第二列,...)c.可以指定行号,列号,切割符,操作后分隔符案例: chkconfig

JS中的call、apply、bind方法

JS中的call.apply.bind方法 一.call()和apply()方法 1.方法定义call方法: 语法:call([thisObj[,arg1[, arg2[,   [,.argN]]]]]) 定义:调用一个对象的一个方法,以另一个对象替换当前对象. 说明: call 方法可以用来代替另一个对象调用一个方法.call 方法可将一个函数的对象上下文从初始的上下文改变为由 thisObj 指定的新对象. 如果没有提供 thisObj 参数,那么 Global 对象被用作 thisObj.

理解JS中的call、apply、bind方法

理解JS中的call.apply.bind方法(*****************************************************************) 在JavaScript中,call.apply和bind是Function对象自带的三个方法,这三个方法的主要作用是改变函数中的this指向. call.apply.bind方法的共同点和区别:apply . call .bind 三者都是用来改变函数的this对象的指向的:apply . call .bind 三者

js中的call()、apply()和bind()方法的区别

call(thisObj,param1,param2....)方法:调用一个对象的方法,用另外的对象去替换当前对象. 下面给出一个例子: function add(a,b){ return a+b; } function sub(a,b){ return a-b; } add.call(sub,3,2) 当执行以上代码时,输出的结果为5.执行add.call(sub,3,2)==add(3,2);相当于用add来替换sub;其实每一个函数也是一个对象,它是Function的实例对象. 再写一个改

【转载】JS中bind方法与函数柯里化

原生bind方法 不同于jQuery中的bind方法只是简单的绑定事件函数,原生js中bind()方法略复杂,该方法上在ES5中被引入,大概就是IE9+等现代浏览器都支持了(有关ES5各项特性的支持情况戳这里ECMAScript 5 compatibility table),权威指南上提到在ES3中利用apply模拟该方法的实现(JS权威指南中函数那章), 但无法真实还原该方法, 这也是真bind方法中的有趣特性. (原文这边理解有问题, 这段话的意思如果结合犀牛书上下文的意思, 再结合犀牛书中

浅谈用原生 js 实现函数的 bind 方法

bind的作用是让目标函数执行时候的 this 改为指定的上下文. 一般情况下,this 取决于调用者,谁调用函数 this 就是谁(自执行函数.定时器--之类的特殊情况除外). 有那么一句话,在 JavaScript 中,万物皆对象. 1 var want = function want() { 2 console.log(this); 3 } 4 5 var good = function good() { 6 console.log('https://www.cnblogs.com/xwa