学习记录之关于javascript的call,apply,bind函数

apply, call, bind是JavaScript中用于改变函数执行上下文(即函数内部的this指向)的方法,他们的存在和使用方式方式反应了关于函数是一等公民以及动态的this绑定规则

产生背景及原因

javascript的this关键字是语言中一个非常特别的概念,它指向函数执行时所在的上下文对象,在不同情况下,this可能指向全局对象、调用它的对象、构造函数创建的新对象,或者在严格模式下为undefined,这种灵活性虽然强大,但也会导致代码难以预测和调试,尤其是在函数作为参数传递或回调使用时。
为了给开发者提供更精细的控制,能够明确指定函数执行时的上下文对象,JavaScript引入的call, apply, bind方法,这些方法允许显示设置函数中的this值,并可以在不同场景下灵活应用

使用

call

call方法立即调用函数,并将函数内的this设置为指定的对象,可以接收多个参数,第一个参数是要绑定给this的对象,后面的参数是直接传递给被调用函数的
调用方式:fn.call(this, arg1, arg2)

    function greet(name) {
        console.log(`Hello, ${name}! I'm ${this.name}.`);
      }

      const person = { name: 'John' };
      greet.call(person, 'Alice')//立即执行,打印 Hello, Alice! I'm John.

apply

与call方法类似,立即调用函数,并将函数内的this设置为指定的对象,apply函数可以接收两个参数,第一个参数是要绑定给this的对象,第二个参数是一个数组,数组中的每一项依次是传递给被调用函数的
调用方式:fn.apply(this,[arg1, arg2])

   function greet(name1,name2) {
        console.log(`Hello, ${name1} and ${name2}! I'm ${this.name}.`);
  }

  const person = { name: 'John' };
  greet.apply(person, ['Alice','Bob']);//立即执行,打印 Hello, Alice and Bob! I'm John.

bind

bind方法不会立即调用函数,而是返回一个新的函数,这个新函数的this被永久绑定到传入bind的第一个参数所指的对象上,,可以提供部分应用一些参数(即部分应用函数),并流程其余参数在之后调用时再填充
调用方式: bind(this, arg1, arg2)

function greet(greeting) {
  console.log(greeting + ', ' + this.name);
}

const person = { name: 'Bob' };
const greetBob = greet.bind(person);

greetBob('Hi'); // 打印 Hi, Bob

实现call方法

思路:相当于在obj上调用fn方法,此时的this是指向obj的,call就是模拟了这个过程,context就相当于obj

	let obj = {
		a: 'pppp',
		fn: function(){
			console.log(this,'fn')//{a: 'pppp', fn: ƒ}a: "pppp"fn: ƒ ()[[Prototype]]: Object 'fn'
		}
	}

实现代码
(context = window, …args)这里使用…args剩余参数,因为不知道调用mycall的函数需要的参数个数,所以使用剩余参数将传入的参数收集,然再调用 contextfnKey的时候使用展开语法将收集的参数一个个拿出来

Function.prototype.mycall = function(context = window, ...args) {//此处的...args是用剩余语法将剩余的参数组成一个新数组
		// args 传递过来的参数 this表示调用了mycall函数的fn,比如greet.mycall(person,'hello'	)中this就是greet函数  context就是调用mycall函数时传入的this
	// 如果是值类型,把他变成对象类型
	if (typeof context !== 'object') {
		context = new Object(context)
	}
	// 设置一个唯一值作为属性名称,避免出现属性名称覆盖
	let fnKey = Symbol();
	// 相当于 obj[fnKey] = this,这里的this就是调用mycall函数的函数
	context[fnKey] = this;
	// 相当于obj.fn()执行,此时的this指向obj,在这里指向的就是context这个传入的this,执行完将结果保存在变量中
	let result = context[fnKey](...args)//此处...args,是将上面用剩余语法得到的那个args数组,展开后,作为函数的参数传递
	// 删除fn, 防止污染(即清掉obj上的fn属性)
	delete context[fnKey]
	// 返回结果
	return result
}

实现apply方法

思路:与实现call方法一致,不过参数的处理不同,(context=window, args)这里直接接收调用myapply时传过来的数组,然后在调用 contextfnKey用展开语法将args里面的每一个取出来
实现代码

Function.prototype.myapply = function(context=window, args){
	// args的处理和call不一样
	if(typeof context != 'object') context = new Object(context)
	let fnKey = Symbol()
	context[fnKey] = this;
	let result = context[fnKey](...args)
	delete context[fnKey];
	return result
				
}

关于展开语法(Spread Syntax)和剩余语法(Rest Syntax)

上面手写call和apply方法的区别在于参数的处理不同。同时又因为展开语法和剩余语法看起来很相似,所以记录区分一下

展开语法(Spread Syntax)

可以在函数调用或数组构造时,将数组表达式或者string在语法层面展开;还可以在构造字面量对象时,将对象表达式按照key-value方式展开。(字面量指[1,2,3]或者{name:‘小明’}这种简洁的构造方式

在函数调用时使用展开语法

function test(a, b,c){
	console.log(a,b,c)
}
let args = [1,2,3]
test(...args)

test(…args)中…args的操作表示将数组agrs数组进行展开操作,展开之后的效果就相当于这样调用test函数:test(1, 2,3),通过打印参数也能看出来

在new表达式中使用展开语法

	function Test(a, b,c){
		console.log(a,b,c)
	}
	var args = [1,2,3]
	new Test(...args)

构造字面量数组时使用展开语法
在没有展开语法的时候只能通过,push,slice,concat等方法,将已有数组元素变为新数组的一部分

var args = [1,2,3]
var arr = ['a','b',...args,'o','p']
console.log(arr)

构造字面量对象的时候使用展开语法
字面量对象的展开特性,是将已有对象的所有可枚举属性拷贝到新构造的对象中
对象的合并,对象的浅拷贝都可以使用展开语法
浅拷贝:只复制最外层的数据结构,对于嵌套的引用类型(数组或对象),仍然共享相同的引用

let obj1 = {
	a: 1,
 	name: 'hello',
 	test:{
		c: 1
	}
}
let obj2 = {b: 2}
// 浅拷贝
let obj1ShallowCopy={...obj1}
console.log(obj1ShallowCopy)
// 合并成为新对象
let mergeObj = {...obj1, ...obj2}
console.log(mergeObj)
obj1.test.c = 100;
obj1ShallowCopy.name = 'hi'
console.log('after the changeof obj1 mergeObj', mergeObj)
console.log('after the changeof obj1', obj1ShallowCopy,obj1)

只能用于可迭代对象
数组或者函数参数中使用展开语法时只能用于可迭代对象

let obj1 = {'key1': 'value1'}
let obj2 = [...obj1]

在这里插入图片描述

剩余语法(Rest Syntax)

看起来和展开语法完全相同,不同在于,剩余参数用于解构数组和对象,从某种意义上来说,剩余语法和展开语法是相反的,展开语法将数组展开味其中的各个元素,而剩余语法则是将多个元素手机起来,并凝聚为单个元素
剩余参数允许我们将一个不定数量的参数表示为一个数组

语法描述

如果函数掉最后一个命名参数以…为前缀,则它将成为一个由剩余参数组成的真数组,其中从0包括到theArgs.length的元素由传递给函数的实际参数提供
function(a, b, …args) {}中,调用时传过来的参数,第一个映射到a,第二个映射到b,剩下的参数都收集到args中

剩余参数和arguments对象的区别

函数的剩余参数和arguments对象都是用来处理不定数量的参数传递给函数的方式,但它们之间是有区别的
**剩余参数:**使用三个点…和一个形参名来定义,它明确表示该函数接受可变数量的参数,并将这些参数收集到一个数组中,剩余参数只能出现在参数列表的最后,也就是剩余参数只接收没有对应形参的实参

	function test(a,b, ...args){
		console.log(a,b, args)//1 2 (3) [3, 4, 5], args是一个真正的数组
	}
	test(1,2,3,4,5)

arguments对象:是一个类数组对象,包含了传递给函数的所有实参,它不是真正意义上的数组,是个类数组,所以不具备数组的方法如push,map,forEach等,

function test(a,b, ...args){
		console.log(arguments)
		console.log(args)
}
test(1,2,3,4,5)

在这里插入图片描述

实现bind方法

可查看:解析 bind 原理,并手写 bind 实现
bind()方法创建一个新的函数,在bind()被调用时,这个新的函数的this被指定为bind()的第一个参数,而其余参数将作为新函数的参数,供调用时使用。与apply,call直接执行函数不同,bind方法返回一个绑定上下文的函数,对于这个函数有两种方式调用,一直是直接调用,另一种是通过new的方式。对于直接调用,通过apply的实现方式,但是对于参数需要注意,bind可以实现类似fn.bind(thisArg,1)(2)因此需要将两边的参数拼接起来;对于通过new 方式调用来说,不会被任何方式改变this,所以对于这周情况要忽略传入的this
箭头函数的底层是bind,无法改变this,只能改变参数

关于bind与new

一个绑定函数也能使用 new 操作符创建对象:这种行为就像把原函数当成构造器,提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。与直接调用构造函数创建实例对象是一样的
bindBottle()中this指向bottle,所以打印’Hello,bottle’,
new bindBottle()调用时,使用new操作符调用一个函数时,它会创建一个新的空对象,并将这个空对象设置胃函数内部发this值,不管之前通过.bind()方法已经设置了什么this值,因此,即使bindBottle已经绑定了bottle作为它的this值,当你使用new操作符时,this会被重置为新创建的对象。由于这个新对象并没有nickname属性,所以输出结果为Hello, undefined。
顺便复习new操作符做了些什么事情
创建一个新对象
对象连接到构造函数原型上,并绑定 this(this 指向新对象)
执行构造函数代码(为这个新对象添加属性)
返回新对象

	this.nickname = 'window'
	let bottle = {
		nickname: 'bottle',
	}
	function sayHello(){
		console.log(`Hello,${this.nickname}`)
	}
	let bindBottle = sayHello.bind(bottle)
	bindBottle();// Hello,bottle
	new bindBottle();// Hello,undefined

作为构造函数使用的绑定函数

function Bottle(nickname) {
	this.nickname = nickname;
}
Bottle.prototype.sayHello = function () {
		console.log('Hello, ', this.nickname)
};

let bottle = new Bottle('bottle');
bottle.sayHello()
let BindBottle = Bottle.bind(null, 'bindBottle');//通过bind方法创建了一个新的函数,,预设了第一个参数是bindBottle

let b1 = new BindBottle('b1');
b1 instanceof Bottle; // true
b1 instanceof BindBottle; // true
new Bottle('bottle1') instanceof BindBottle; // true
b1.sayHello()// Hello,  bindBottle

new BindBottle(‘b1’)当使用new关键字调用BindBottle时,JavaScript引擎做了以下几件事
1、创建一个新的空对象
2、将这个空对象的原型设置味Bottle.prototype(即继承自Bottle)
3、将this绑定到这个新对象
4、执行Bottle构造函数,但是,此处nickname参数已经被.bind()设置为了bindBottle,即使传递了b1,它也会被忽略
5、返回这个新对象

let value = 2;
let foo = {
    value: 1
};
function bar(name, age) {
    return {
		value: this.value,
		name: name,
		age: age
    }
};

bar.call(foo, "Jack", 20); // 直接执行了函数
// {value: 1, name: "Jack", age: 20}

let bindFoo1 = bar.bind(foo, "Jack", 20); // 返回一个函数
bindFoo1();
// {value: 1, name: "Jack", age: 20}

let bindFoo2 = bar.bind(foo, "Jack"); // 返回一个函数
bindFoo2(20);
// {value: 1, name: "Jack", age: 20}
function foo(x, y, z) {
		this.name = "张三";
		console.log(this.num, x + y + z);
}
var obj = {
	num: 666
}
var foo2 = foo.bind(obj, 1,2,3);
console.log(new foo2());

在这里插入图片描述
实现

 Function.prototype.mybind = function (context = window, ...args) {
        if (typeof this !== "function") {
          throw newTypeErrot("is not a function");
        }
        let self = this; //fn.bind(thisArg, a), self 就是fn
        let fBound = function (...innerArgs) {
          /**
           * 对两种调用情况进行处理
           * 1、如果是通过new关键字调用fBound,会创建一个新的实例,根据JavaScript的行为规范,
           * 构造函数应该初始并返回一个新的对象实例,此时的this指向新创建的实例,此处如果也把context作为apply的参数,
           * 那么新创建的对象实例就不能被正确的初始化,也不会继承构造函数的原型属性和方法
           * 2、如果是正常调用,那么apply的第一个参数就是context,然后通过concat将内外层参数拼接起来,作为apply的第二个参数
        
          */

          //
          return self.apply(
            this instanceof fBound ? this : context,
            args.concat(innerArgs)
          );
        };
        /**
         * 通过new方式调用fBound,那么需要继承构造函数原型属性和方法:保证源函数的原型对象上的属性不丢失
         */
        fBound.prototype = Object.create(self.prototype);
        return fBound;
      };

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值