Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

如何写一个实用的bind? #7

Open
qianlongo opened this issue Apr 29, 2018 · 0 comments
Open

如何写一个实用的bind? #7

qianlongo opened this issue Apr 29, 2018 · 0 comments

Comments

@qianlongo
Copy link
Owner

前言

这是underscore.js源码分析的第五篇,如果你对这个系列感兴趣,欢迎点击

underscore-analysis/ watch一下,随时可以看到动态更新。

事情要从js中的this开始说起,你是不是也经常有种无法掌控和知晓它的感觉,对于初学者来说,this简直如同回调地狱般,神乎其神,让人无法捉摸透。但是通过原生js中的bind方法,我们可以显示绑定函数的this作用域,而无需担心运行时是否会改变而不符合自己的预期。当然了下划线中的bind也是模仿它的功能同样可以达到类似的效果。

ctx

bind简单回顾

我们从mdn上的介绍来回顾一下bind的使用方法。

bind方法创建一个新的函数, 当被调用时,它的this关键字被设置为提供的值。

语法

fun.bind(thisArg[, arg1[, arg2[, ...]]])

简单地看一下这些参数的含义

  1. thisArg

当绑定函数被调用时,该参数会作为原函数运行时的this指向,当使用new 操作符调用绑定函数时,该参数无效。

  1. arg1, arg2, ...

当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。

绑定this作用域示例

window.name = 'windowName'

let obj = {
  name: 'qianlongo',
  showName () {
    console.log(this.name)
  }
}

obj.showName() // qianlongo

let showName = obj.showName
  showName() // windowName

let bindShowName = obj.showName.bind(obj)
  bindShowName() // qianlongo

通过以上简单示例,我们知道了第一个参数的作用就是绑定函数运行时候的this指向

第二个参数开始起使用示例

let sum = (num1, num2) => {
  console.log(num1 + num2)
}

let bindSum = sum.bind(null, 1)
bindSum(2) // 3

bind可以使一个函数拥有预设的初始参数。这些参数(如果有的话)作为bind的第二个参数跟在this(或其他对象)后面,之后它们会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们的后面。

参数的使用基本上明白了,我们再来看看使用new去调用bind之后的函数是怎么回事。

function Person (name, sex) {
  console.log(this) // Person {}
  this.name = name
  this.sex = sex
}
let obj = {
  age: 100
}
let bindPerson = Person.bind(obj, 'qianlongo')

let p = new bindPerson('boy')

console.log(p) // Person {name: "qianlongo", sex: "boy"}

有没有发现bindPerson内部的this不再是bind的第一个参数obj,此时obj已经不再起效了。

实际上bind的使用是有一定限制的,在一些低版本浏览器下不可用,你想不想看看下划线中是如何实现一个兼容性好的bind呢!!!come on

下划线中bind实现

源码

 _.bind = function(func, context) {
  // 如果原生支持bind函数,就用原生的,并将对应的参数传进去
  if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1));
  // 如果传入的func不是一个函数类型 就抛出异常
  if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
  // 把第三个参数以后的值存起来,接下来请看executeBound
  var args = slice.call(arguments, 2);
  var bound = function() {
    return executeBound(func, bound, context, this, args.concat(slice.call(arguments)));
  };
  return bound;
};

executeBound实现

var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
  // 如果调用方式不是new func的形式就直接调用sourceFunc,并且给到对应的参数即可
  if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 
  // 处理new调用的形式
  var self = baseCreate(sourceFunc.prototype);
  var result = sourceFunc.apply(self, args);
  if (_.isObject(result)) return result;
  return self;
};

上面的源码都做了相应的注释,我们着重来看一下executeBound的实现

先看一下这些参数都代表什么含义

  1. sourceFunc:原函数,待绑定函数
  2. boundFunc: 绑定后函数
  3. context:绑定后函数this指向的上下文
  4. callingContext:绑定后函数的执行上下文,通常就是 this
  5. args:绑定后的函数执行所需参数

ok,我们来看一下第一句

if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 

这句话是为了判断绑定后的函数是以new关键字被调用还是普通的函数调用的方式,举个例子

function Person () {
  if (!(this instanceof Person)) {
    return console.log('非new调用方式')
  }

  console.log('new 调用方式')
}

Person() // 非new调用方式
new Person() // new 调用方式

所以如果你希望自己写的构造函数无论是new还是没用new都起效的话可以用下面的代码

function Person (name, sex) {
  if (!(this instanceof Person)) {
    return new Person(name, sex)
  }

  this.name = name
  this.sex = sex
}

new Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}

Person('qianlongo', 'boy') // Person {name: "qianlongo", sex: "boy"}

我们回到executeBound的解析

if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); 

callingContext是被绑定后的函数的this作用域,boundFunc就是那个被绑定后的函数,那么通过这个if判断,当为非new调用形式的时候,直接利用apply处理即可。

但是如果是用new调用的呢?我们看下面这段代码,别看短短的四行代码里面知识点挺多的呢!

// 这里拿到的是一个空对象,且其继承于原函数的原型链prototype
var self = baseCreate(sourceFunc.prototype);
// 构造函数执行之后的返回值
// 一般情况下是没有返回值的,也就是undefined
// 但是有时候写构造函数的时候会显示地返回一个obj
var result = sourceFunc.apply(self, args);
// 所以去判断结果是不是object,如果是那么返回构造函数返回的object
if (_.isObject(result)) return result;
// 如果没有显示返回object,就返回原函数执行结束后的实例
return self;

好,到这里,我有一个疑问,baseCreate是个什么鬼?

var Ctor = function(){};

var baseCreate = function(prototype) {
  // 如果prototype不是object类型直接返回空对象
  if (!_.isObject(prototype)) return {};
  // 如果原生支持create则用原生的
  if (nativeCreate) return nativeCreate(prototype); 
  // 将prototype赋值为Ctor构造函数的原型
  Ctor.prototype = prototype; 
  // 创建一个Ctor实例对象
  var result = new Ctor; 
  // 为了下一次使用,将原型清空
  Ctor.prototype = null; 
  // 最后将实例返回
  return result; 
};

是不是好简单,就是实现了原生的Object.create用来做一些继承的事情。

结尾

文章很简短,知道怎么实现一个原生的bind就行。如果你对apply、call和this感兴趣,欢迎查看

js中call、apply、bind那些事

this-想说爱你不容易

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant