JavaScript简餐——那些函数属性与方法(call、apply和bind)

本文是《JavaScript简餐》系列的一部分,详细介绍了arguments对象的callee属性、函数的caller属性、new.target特性,以及重点解析了call、apply和bind这三种函数调用方式,帮助读者深入理解JavaScript中的函数操作。
摘要由CSDN通过智能技术生成


前言

写本《JavaScript简餐》系列文章的目的是记录在阅读学习《JavaScript高级程序设计(第4版)》一书时出现的各个知识点。虽是对读书的笔记和总结,但是希望它轻量、简洁、犀利,不会引起阅读疲劳,可以在碎片化时间和闲暇之余轻巧地沐浴一下知识点。每篇文章只针对一个小部分进行讲解式的梳理,来达到个人复习总结和分享知识的目的。


一、arguments对象的callee属性

关于arguments对象,前面已经详细介绍过了,它是一个类数组对象,包含调用函数时传入的所有参数。其中有一个比较有意思的属性叫callee,这是指向arguments对象所在函数的指针。直接来看一下具体例子来理解一下:
function person() {
  console.log(arguments.callee);
}

person(); 
//输出结果就是这个函数:
// ƒ person() {
//   console.log(arguments.callee);
// }
这个属性一个实用的地方是在递归函数中将函数逻辑与函数名解耦。来看一个用递归计算阶乘的例子:
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * factorial(num - 1);
  }
}
这个例子中,这个函数要正确执行就必须保证函数名是factorial,从而导致了紧密耦合。那么,我们就可以使用arguments.callee来让函数逻辑与函数名解耦:
function factorial(num) {
  if (num <= 1) {
    return 1;
  } else {
    return num * arguments.callee(num - 1); // 这里我们将factorial换成arguments.callee来代替函数名
  }
}
这样就意味着无论函数叫什么名称,都可以引用正确的函数。再来考虑下面这种函数被中途修改的情况:
let anotherFactorial = factorial;

factorial = function () {
  return 0;
};

console.log(anotherFactorial(5)); // 120
console.log(factorial(5)); // 0
在这里,anotherFactorial被赋值为刚刚定义的factorial函数,而后面factorial函数被重写为一个返回0的函数。如果像我们第一次的那个版本,在递归函数内使用factorial函数进行递归而不是使用arguments.callee,那么这两个函数的运行结果就都是0。不过将函数逻辑与函数名称解耦后,anotherFactorial函数就依旧可以返回正确的阶乘运算结果。

二、函数的caller属性

caller这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null:
function world() {
  person();
}

function person() {
  console.log(person.caller);
}

world();
//输出结果就是调用person函数的earth函数:
// ƒ world() {
//   person();
// }
当然,这里如果想像前面那样降低耦合性的话我们还是可以用arguments.callee来代替person:
function world() {
  person();
}

function person() {
  console.log(arguments.callee.caller);
}

world();
//输出结果就是调用person函数的earth函数:
// ƒ world() {
//   person();
// }

结果是一样的。


三、new.target

ECMAScript中的函数既可以当作普通函数来调用,也可以当作构造函数来实例化一个对象。在ECMAScript6中新增了检测函数是否使用了new关键字调用的new.target属性。如果函数是正常调用的,那么在函数中调用new.target则会返回undefined,如果函数是使用new关键字来调用的,那么new.target将会引用被调用的构造函数。来看一个具体例子:
function Person(name, age) {
  if (new.target === Person) {
    console.log("当前函数被当作构造函数使用来初始化一个实例!");
  } else {
    console.log("当前函数被当作普通函数调用。");
  }
}

new Person(); // 当前函数被当作构造函数使用来初始化一个实例!
Person(); // 当前函数被当作普通函数调用。

四、call、apply和bind方法

call、apply、bind方法都是用来改变函数体内this对象的指向的,同时还具备传参的功能,我们一个个看过来:

1.call方法

还是直接通过代码实例来理解一下吧:
//实例1
const person = {
  name: "Lucy",
  age: 20,
};

function sayInformation() {
  console.log(this.name);
  console.log(this.age);
}

sayInformation.call(person);
// Lucy
// 20

//实例2
function doAdd(x, y) {
  return x + y;
}

function add(x, y) {
  return doAdd.call(this, x, y);
}

console.log(add(1, 2)); // 3
在实例1中person对象中没有定义任何方法,打印信息的函数我们在全局中定义了一个sayInformation函数,可是这个函数打印的是this.name和this.age而不是person.name和person.age。那如果我们想打印person对象中的name和age属性要怎么办呢?那当然是要在函数和对象之间搭个桥了。这个时候call方法就派上用场了,就用它来搭桥。原理就是让sayInformation函数的this对象指向person。所以在最后我们调用的是sayInformation.call(person)。这样就可以打印出person中的属性了。


在实例2中,我们想进行加法运算,于是定义了一个add方法,但是我们不想让这个add函数干活,想让别的函数替它进行运算,所以我们定义了doAdd方法,让它来执行运算。此时我们就可以利用call方法将参数传给doAdd。在doAdd.call(this, x, y)这句中会将doAdd函数的this对象指向add函数的this(在这里this是window,因为add是在全局window对象中被调用的),之后将add接收到的参数x和y传给doAdd,即1和2。最终,我们得到相加的结果3。(当然直接return doAdd(x, y)也是可以的哈,在这里主要是为了演示call方法的传参功能。)

2.apply方法

apply在功能上与call方法是等价的,只不过在传参的方法上不同。call方法需要一个一个传参,而apply方法需要传入参数数组。将上面的例子改成利用apply方法的版本如下:
//实例1
const person = {
  name: "Lucy",
  age: 20,
};

function sayInformation() {
  console.log(this.name);
  console.log(this.age);
}

sayInformation.apply(person);
// Lucy
// 20

//实例2
function doAdd(x, y) {
  return x + y;
}

function add(x, y) {
  return doAdd.apply(this, [x, y]); // 在这里不是逐个传参了,而是传一个参数数组
}

console.log(add(1, 2)); // 3
到底是使用apply还是call,完全取决于怎么给要调用的函数传参更方便。如果想直接传arguments对象或者一个数组,那就用apply();否则就用call()。如果不用给被调用的函数传参,则用哪种方法都一样。

3.bind方法

ECMAScript5出于同样的目的定义了一个新方法:bind()。bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind的对象。其实换汤不换药,来看以下例子:
const numbers = {
  x: 1,
  y: 2,
};

function add(num1, num2) {
  return this.x * num1 + this.y * num2;
}

let getResult = add.bind(numbers, 2, 3);
console.log(getResult()); // 8
在这个例子中我们定义了numbers对象,其中有属性x和y。之后我们又定义了add函数,函数的功能是将x和y分别乘以一个数字并返回。主要看下面的部分,我们先利用bind方法将add函数的this指向numbers对象,顺便传了两个参数2和3(注意,这里的参数是逐个传的,和call方法相同)。这会创建一个新的函数实例,之后我们将这个新产生的函数实例赋值给getResult变量,在最后我们通过getResult()来调用这个函数。当然最后一部分也可以将两句简写成一句let getResult = add.bind(numbers, 2, 3) ()来直接调用。


将前面的call和apply的两个例子写成bind的版本如下:
//实例1
const person = {
  name: "Lucy",
  age: 20,
};

function sayInformation() {
  console.log(this.name);
  console.log(this.age);
}

sayInformation.bind(person)();
// Lucy
// 20

//实例2
function doAdd(x, y) {
  return x + y;
}

function add(x, y) {
  return doAdd.bind(this, x, y)(); // 在这里利用bind,并且直接调用函数。参数逐个传
}

console.log(add(1, 2)); // 3

总结

以上就是今天要讲的内容,今天比较详细地介绍了arguments对象的callee属性、函数中的caller属性、new.target属性以及如何利用call、apply和bind方法来改变函数的this指向。下一篇,我们来讲一下JavaScript中的闭包。撒花~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值