前端知识体系
GitHub (opens new window)

GuoLiBin6

程序员永不下班
GitHub (opens new window)
  • 介绍
  • 前端基础

    • CSS

    • JavaScript

      • 进程、线程、协程
      • call、apply、bind
        • call 和 apply
          • call和apply的区别
          • 用途
          • 改变this指向
          • Function.prototype.bind
          • 借用其他对象的方法
          • 实现call
          • 实现apply
        • bind
          • 实现bind
          • 实现bind总结
      • ES6

    • HTML

  • 浏览器基础

  • 软件开发

  • 数据结构

  • 性能优化

  • Node.js

  • 收录

  • 搞事啦

  • 前端知识体系
  • 前端基础
  • JavaScript
GuoLiBin6
2024-01-03
目录

call、apply、bind

# call 和 apply

# call和apply的区别

Function.prototype.call 和 Function.prototype.apply 都是非常常用的方法。它们的作用一模一样,区别仅在于传入参数形式的不同。

apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数:

var func = function( a, b, c ){ 
    alert ( [ a, b, c ] );    // 输出 [ 1, 2, 3 ] 
}; 
func.apply( null, [ 1, 2, 3 ] ); 
1
2
3
4

call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数:

var func = function( a, b, c ){ 
    alert ( [ a, b, c ] );    // 输出 [ 1, 2, 3 ] 
}; 
func.call( null, 1, 2, 3 );  
1
2
3
4

当调用一个函数时,JavaScript 的解释器并不会计较形参和实参在数量、类型以及顺序上的区别,JavaScript 的参数在内部就是用一个数组来表示的。从这个意义上说,apply 比 call 的使用率更高,我们不必关心具体有多少参数被传入函数,只要用 apply 一股脑地推过去就可以了。

call 是包装在 apply 上面的一颗语法糖,如果我们明确地知道函数接受多少个参数,而且想一目了然地表达形参和实参的对应关系,那么也可以用 call 来传送参数。

有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法。那么我们可以传入 null 来代替某个具体的对象:

Math.max.apply( null, [ 1, 2, 5, 3, 4 ] )    // 输出:5 
1

# 用途

# 改变this指向

var obj1 = { 
    name: 'sven' 
}; 
var obj2 = { 
    name: 'anne' 
}; 
window.name = 'window'; 
var getName = function(){ 
    alert ( this.name ); 
}; 
getName();    // 输出: window 
getName.call( obj1 );    // 输出: sven 
getName.call( obj2 );    // 输出: anne 
1
2
3
4
5
6
7
8
9
10
11
12
13

在实际开发中,经常会遇到 this 指向被不经意改变的场景,比如有一个 div 节点,div 节点的 onclick 事件中的 this 本来是指向这个 div 的:

document.getElementById( 'div1' ).onclick = function(){ 
    alert( this.id );        // 输出:div1 
}; 
1
2
3

假如该事件函数中有一个内部函数 func,在事件内部调用 func 函数时,func 函数体内的 this 就指向了 window,而不是我们预期的 div,见如下代码:

document.getElementById( 'div1' ).onclick = function(){ 
    alert( this.id );            // 输出:div1 
    var func = function(){ 
        alert ( this.id );        // 输出:undefined 
    } 
    func(); 
}; 
1
2
3
4
5
6
7

这时候我们用 call 来修正 func 函数内的 this,使其依然指向 div:

document.getElementById( 'div1' ).onclick = function(){ 
    var func = function(){ 
        alert ( this.id );        // 输出:div1 
    } 
    func.call( this ); 
}; 
1
2
3
4
5
6

# Function.prototype.bind

Function.prototype.bind,用来指定函数内部的 this指向

实现bind

Function.prototype.bind = function( context ){ 
    var self = this;        // 保存原函数 
    return function(){        // 返回一个新的函数 
        return self.apply( context, arguments );    // 执行新的函数的时候,会把之前传入的 context 
                                                    // 当作新函数体内的 this 
    } 
}; 
var obj = { 
    name: 'sven' 
}; 
var func = function(){ 
    alert ( this.name );    // 输出:sven 
}.bind( obj); 
func(); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14

我们通过 Function.prototype.bind 来“包装”func 函数,并且传入一个对象 context 当作参数,这个 context 对象就是我们想修正的 this 对象。

在 Function.prototype.bind 的内部实现中,我们先把 func 函数的引用保存起来,然后返回一个新的函数。当我们在将来执行 func 函数时,实际上先执行的是这个刚刚返回的新函数。在新函数内部,self.apply( context, arguments )这句代码才是执行原来的 func 函数,并且指定 context 对象为 func 函数体内的 this。

这是一个简化版的 Function.prototype.bind 实现,通常我们还会把它实现得稍微复杂一点,使得可以往 func 函数中预先填入一些参数:

Function.prototype.bind = function(){  
    var self = this,    // 保存原函数 
        context = [].shift.call( arguments ),    // 需要绑定的 this 上下文 
        args = [].slice.call( arguments );    // 剩余的参数转成数组 
    return function(){    // 返回一个新的函数 
        return self.apply( context, [].concat.call( args, [].slice.call( arguments ) ) ); 
            // 执行新的函数的时候,会把之前传入的 context 当作新函数体内的 this 
            // 并且组合两次分别传入的参数,作为新函数的参数 
        } 
    }; 
var obj = { 
    name: 'sven' 
}; 
var func = function( a, b, c, d ){ 
    alert ( this.name );        // 输出:sven 
    alert ( [ a, b, c, d ] )    // 输出:[ 1, 2, 3, 4 ] 
}.bind( obj, 1, 2 ); 
func( 3, 4 ); 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 借用其他对象的方法

场景一

借用构造函数实现继承

var A = function (name) {
	this.name = name
}
var B = function () {
	A.apply(this, arguments)
}
B.prototype.getName = function () {
	return this.name
}
var b = new B('even')
b.getName()  // even
1
2
3
4
5
6
7
8
9
10
11

场景二

参数arguments借用Array的能力

(function(){ 
    Array.prototype.push.call( arguments, 3 ); 
    console.log ( arguments );    // 输出[1,2,3] 
})( 1, 2 ); 
1
2
3
4

# 实现call

# 实现apply

# bind

先看使用方法

const obj1 = {
	name: 'lili'
}
const obj2 = {
	name: 'xiaoming'
}
window.name = 'window'

function who (text) {
	console.log(`我是${this.name}, ${text}`)
}

who('我是工具') // 我是window,我是工具

const fn1 = who.bind(obj1)
fn1('我是女孩') // 我是lili,我是女孩

const fn2 = who.bind(obj2, '我是男孩')
fn2() // 我是xiaoming,我是男孩
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

由上看出几点:

  • bind()函数执行,不会执行原函数,而是返回一个新函数,新函数执行时才会执行原函数
  • bind()可以改变this的指向,第一次执行时,who为普通函数,this指向window,后两次输出分别指向obj1和obj2
  • bind()接受多个参数,第一个参数为原函数执行过程中this的指向,从第二个参数开始,参数是可选的,可以在bind时传入,也可以在返回的新函数执行时传入

原函数参数及顺序,是bind时第二个参数开始的参数+新函数执行时传入的参数顺序,多传入的参数不生效。利用这个特性,可以使一个函数拥有预设的初始参数,看代码:

function list () {
	return Array.prototype.slice.call(arguments)
}

const list1 = list(1, 2, 3) // [1, 2, 3]

const paramToList = list.bind(null, 5)

const lsit2 = paramToList(1, 2) // [5, 1, 2]
const list3 = paramToList() // [5]
1
2
3
4
5
6
7
8
9
10

这样,在bind时传入的参数5,就会作为paramToList函数的预设参数,在调用时总是出现在第一个参数位置。

# 实现bind

既然已经知道了 bind() 函数的作用,我们就可以通过它的作用,慢慢还原它的实现经过

首先,biind() 调用是 fun.bind(obj),直接由一个函数发起调用,且函数上没有这个方法,证明 bind() 存在于函数的原型链上,如下

Function.prototype.bind = function (obj) {
	//
}
1
2
3

其次,bind() 函数执行不会执行原函数,会返回一个新函数,新函数执行时才会执行原函数,即,bind中返回的函数执行时,才会打印“执行”,如下

Function.prototype.bind = function (obj) {
	// 保存原函数 
    // bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
	const self = this
	// 返回新函数
	return function () {
		// 需要在这里执行原函数
		self()
	}
}
1
2
3
4
5
6
7
8
9
10

实际使用一下看看,如下

Function.prototype.bind = function (obj) {
	// 保存原函数 
    // bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
	const self = this
	// 返回新函数
	return function () {
		// 需要在这里执行原函数
		self()
	}
}

const person = {
	legs: 2
}

function showLegs () {
	console.log('执行')
	console.log('legs', this.legs)
}

const func = showLegs.bind(person)
func() // 执行 // legs undefined
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

然后,bind() 返回了一个函数,函数再执行,执行的是原函数,但是原函数中的this并没有被绑定到person,要想将 this 绑定到 person,要在新函数,即 func 执行时做改变 this 指向的操作,this 由指向 func 改为指向 person,如下

Function.prototype.bind = function (obj) {
	// 保存原函数 
	// bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
	const self = this
	// 返回新函数
	return function () {
		// 需要在这里执行原函数,并且改变this指向
		self.apply(obj)
	}
}

const person = {
	legs: 2
}

function showLegs () {
	console.log('legs', this.legs)
}

const func = showLegs.bind(person)
func() 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

到这里,bind() 就成功实现了,但是我们还 bind() 可以接受多个参数,从第二个开始作为参数传入,而且返回的新函数中也可以传入参数,所以我们还需要将实现优化,支持传入参数,如下

// 最终版
Function.prototype.bind = function () {
	// 保存原函数 
	// bind作为对象的方法调用,this指向调用方法的对象,即执行 func.bind(obj) 时,this 指向 func
	const self = this
	// 拆解参数
	const obj = [].shift.call(arguments)
	const args = [].slice.call(arguments)
	// 返回新函数
	return function () {
		// 拆解参数
		const allArgs = [].concat.call(args, [].slice.call(arguments))
		// 需要在这里执行原函数,并且改变this指向
		self.apply(obj, allArgs)
	}
}

const person = {
	legs: 2
}

function showLegs (name, endText) {
	console.log('legs', this.legs)
	console.log(`${name} has ${this.legs} legs, ${endText}`)
}

const func = showLegs.bind(person, 'lili')
func(' right') // lili has 2 legs, right
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# 实现bind总结

上述过程中,我们由bind的作用推导出bind的实现,根据以上代码查看下面的一些要点,帮助我们更快的实现bind

  • bind作用:返回一个新函数func,新函数func执行时执行被改变了this指向的原函数showLegs
  • bind实现:返回新函数,在新函数中改变原函数指向
#JavaScript
上次更新: 2024-01-12 19:09:34
进程、线程、协程
let const

← 进程、线程、协程 let const→

最近更新
01
蛤蟆先生去看心理医生
04-12
02
梁永安:阅读、游历和爱情
03-20
03
阿甘正传
02-07
更多文章>
Theme by Vdoing | Copyright © 2022-2024 GuoLiBin6
冀ICP备2022013865号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式