深入函数(下)

文章目录

这些年来,ES6 将 JavaScript 的可用性提升到一个新的水平时: 箭头函数、类等等,这些都很棒。箭头函数是最有价值的新功能之一,有很多好文章描述了它的上下文透明性和简短的语法。

本篇咱们来了解一下箭头函数的基础知识以及一些需要注意的事项。

箭头有很优点,分别如下:

箭头函数的优点

优点1:语法更简短

咱们来看看一个常规函数定义:

function funcName(params) {
   return params + 2;
 }
funcName(2);
// 4

咱们可以使用箭头函数来定义上面的方法,只需一行代码就能搞定:

var funcName = (params) => params + 2
funcName(2);
// 4

很酷,是吧。接着咋个更深入地看一下箭头函数的语法:

(parameters) => { statements }

如果没有参数,可以用如下形式表示:

() => { statements }

只有一个参数时,左边括号是可选的:


parameters => { statements }

最后,如果要返回一个表达式,则需要删除方括号即可:

parameters => expression

// 等价于:

function (parameters){
  return expression;
}
优点2:没有绑定 this

与常规函数不同,箭头函数不会对this进行绑定。 相反,这是按词法绑定的(即,this 含义与其原始上下文无关)。

来个事例,如下所示,咱们创建一个构造函数,然后创建一个实例:

function Counter() {
  this.num = 0
}
var a = new Counter()

使用构造函数会将 this 绑定到要创建的新对象上,在本例中为a对象。 这就是为什么可以通过console.log(a.num)并得到 0

console.log(a.num)
// 0

如果咱们想要每秒增加a.num 的值呢? 咱们可以使用setInterval()函数。setInterval()是一个在设定的毫秒数之后调用另一个函数的函数。咱们把它加到Counter 函数中:

function Counter() {
  this.num = 0
  this.timer = setInterval(function add() {
    this.num++
    console.log(this.num)
  }, 1000)
}

代码看起来和以前一样,除了添加了一个变量,每1秒代码将运行一次,this.num 增加1,然后被打印到控制台。咱们来试试:

var b = new Counter();
// NaN
// NaN
// NaN
// ...

正如看到的,该函数每秒会打印值,但是结果并不是我们所期望的。那么,哪里出错了呢?

咱们的setInterval函数没有在一个声明的对象上被调用。它也没有被new关键字调用(只有Counter()函数)。咱们也没有使用callbindapplysetInterval是一个普通函数。事实上,setIntervalis中的 this 被绑定到全局对象,咱们可以把 this 值打印出来看看:

function Counter() {
  this.num = 0
this.timer = setInterval(function add() {
    console.log(this)
  }, 1000)
}
var b = new Counter()

可以看到 window 对象每秒被打印一次。

回到原来的函数,打印 NaN 是因为 this.num 中的this 指向的window对象(window.num 不存在),而不是咱们创建的b对象(b.num)。

那么咱们如何解决这个问题呢? 最简单的方法,当然是使用箭头函数。咱们需要一个不绑定this的函数。使用箭头函数,this绑定将其原始绑定与上下文隔离,使用箭头函数改造一下:

function Counter() {
  this.num = 0
  this.timer = setInterval(() => {
    this.num++
    console.log(this.num)
  }, 1000)
}
var b = new Counter()
// 1
// 2
// 3
// ...

至此,咱们的的 Counter 函数就能正常工作了。

箭头函数与常规函数行为的差异

箭头函数的语法非常简单,但与常规函数也有一些行为差异,接着来看看这些差异,以及如何利用这些差异来更好使用箭头函数。

  • 无论在严格模式还是非严格模式下,箭头函数都不能具有重复的命名参数。

  • 箭头函数没有arguments绑定。但是,它们可以访问最接近的非箭头父函数的arguments对象。

  • 箭头函数永远不能用作构造函数,自然的不能使用new关键字调用它们,因此,对于箭头函数不存在prototype属性。

  • 在函数的整个生命周期中,箭头函数内部的值保持不变,并且总是与接近的非箭头父函数中的值绑定。

  • 箭头函数中的this的值在函数的整个生命周期中保持不变,并且始终与最接近的非箭头父函数中的this值绑定。

命名函数参数

JS 中的函数通常用命名参数定义,命名参数根据位置将参数映射到函数作用域中的局部变量。

来看看下面的函数:

function logPersonInfo (name, age, sex) {
  console.log(name, age, sex)
}

logPersonInfo('小智', 18, '男')  // 小智 18 男

logPersonInfo({name: '小智'}, [ 18, 27, 32 ])  // {name: "小智"} [18, 27, 32] undefined

logPersonInfo()函数由三个命名参数定义: nameagesex。如果命名参数多于传递给函数的参数,则其余参数undefined

对于命名参数,JS 函数在非严格模式下表现出奇怪的行为。在非严格模式下,JS 函数允许有重复命名参数,来看看示例:

function logPersonInfo (name, age, name) {
  console.log(name, age)
}

// name => '小智'
// age => 18
// name => '男'
logPersonInfo('小智', 18, '男') // "男"  18

// name => { o: 3 }
// age => [ 1, 2, 3 ]
// name => undefined
logPersonInfo({name: '小智'}, [ 18, 27, 32 ])  // undefined  [18, 27, 32 ]

咱们可以看到,first参数重复了,因此,它被映射到传递给函数调用的第三个参数的值,覆盖了第一个参数,这不是一个让人喜欢的的行为。

// 由于参数重复,严格模式会报错
function logPersonInfo (name, age, name) {
  "use strict"
  console.log(name, age);
}

// Error: Duplicate parameter name not allowed in this context
箭头函数如何处理重复的参数

与常规函数不同,无论在严格模式还是非严格模式下,箭头函数都不允许重复参数,重复的参数将引发语法错误。

// 只要你敢写成重复的参数,我就敢死给你看
const logPersonInfo = (name, age, name) => {
  console.log(name, age)
}
函数重载

函数重载是定义函数的能力,这样就可以根据不同的参数数量来调用对应的函数, JS 中可以利用绑定方式来实现这一功能。

来看个简单的重载函数,对传入参数求和:

function sum() {
  const length = arguments.length;
  if (length === 0) return 0

  // 将参数转换数组
  const numbers = Array.prototype.slice.call(arguments)

  // 返回数组元素的总和
  return numbers.reduce((sum, num) => {
    return sum += Number(num)
  }, 0)
}

这样函数可以用任意数量的参数调用,从0到函数可以接受的最大参数数量应该是255

sum() // 0
sum('3o', 4, 5) // NaN
sum('1', 2, '3', 4, '5', 6, 7, 8, 9, 10) // 55
sum(1.75, 2.25, 3.5, 4.125, 5.875) // 17.5

现在尝试使用剪头函数语法来实现sum,开始咱们可能有可能会觉得这有啥难的,无非就换个箭头的写法:

const sum = () => {
  const length = arguments.length;
  if (length === 0) return 0

  // 将参数转换数组
  const numbers = Array.prototype.slice.call(arguments)

  // 返回数组元素的总和
  return numbers.reduce((sum, num) => {
    return sum += Number(num)
  }, 0)
}

现在测试这个函数时,它会抛出一个引用错误,arguments 未定义。

与常规函数不同,arguments不存在于箭头函数中。但是,可以访问非箭头父函数的arguments对象。

function sum() {
  return (() => {
    const length = arguments.length;
    if (length === 0) return 0

    // 将参数转换数组
    const numbers = Array.prototype.slice.call(arguments)

    // 返回数组元素的总和
    return numbers.reduce((sum, num) => {
      return sum += Number(num)
    }, 0)
  })()
}

这样就可以解决了arguments对象没有定义的问题,但这种做法显然很多余。

对于上面问题是否存在替代方法?,可以使用 es6 的 rest 参数。

使用ES6 rest 参数,咱们可以得到一个数组,该数组保存了传递给该函数的所有的参数。rest语法适用于所有类型的函数,无论是常规函数还是箭头函数。

const sum = (...args) => {
  if (args.length === 0) return 0
  
  // 返回数组元素的总和
  return args.reduce((sum, num) => {
    return sum += Number(num)
  }, 0)
}

对于使用rest参数需要注意一些事项:

  • rest参数与函数内部的arguments对象不同。rest参数是一个实际的函数参数,而arguments对象是一个绑定到函数作用域的内部对象。

  • 一个函数只能有一个rest参数,而且它必须位于最后一个参数,这说明着函数可以包含命名参数和rest参数的组合。

  • rest 参数与命名参数一起使用时,它不包含所有传入的参数。但是,当它是惟一的函数参数时,表示函数参数。另一方面,函数的arguments对象总是捕获所有函数的参数。

  • rest参数指向包含所有捕获函数参数的数组对象,而arguments对象指向包含所有函数参数的类数组对象。

构造函数

可以使用new关键字调用常规 JS 函数,该函数作为类构造函数用于创建新的实例对象。

function Person (name = '小智') {
  this.name = name

  this.getName = function() {
    return `我的名字是${this.name}`
  }
}

const person = new Person()

console.log(person.name) // 小智
console.log(person.getName()) // 我的名字是小智

console.log(typeof person) // "object"
console.log(person instanceof Person) // true

使用new关键字调用常规 JS 函数时,将调用函数内部[[Construct]]方法来创建一个新的实例对象并分配内存。之后,函数体将正常执行,并将this映射到新创建的实例对象。最后,函数隐式地返回 this(新创建的实例对象),只是在函数定义中指定了一个不同的返回值。

外,所有常规 JS 函数都有一个prototype属性。函数的prototype属性是一个对象,它包含函数创建的所有实例对象在用作构造函数时共享的属性和方法。

下面咱们对 Person 函数做一个小修改,这次从原型上扩展方法,而不是从构造函数:

function Person (name = '小智') {
  this.name = name
}

Person.prototype.getName = function() {
  return `我的名字是${this.name}`
}

const person = new Person()

console.log(person.name) // 小智
console.log(person.getName()) // 我的名字是小智

console.log(typeof person) // "object"
console.log(person instanceof Person) // true

事实上,ES6 类在后台执行类似于上面代码片段的操作 - 类(class)只是个语法糖

那么箭头函数呢?它们是否也与常规 JS 函数有此行为? 答案是否定的。关于箭头函数:

与常规函数不同,箭头函数永远不能使用new关键字调用,因为它们没有[[Construct]]方法。 因此,箭头函数也不存在prototype属性。

箭头函数不能用作构造函数,无法使用new关键字调用它们,如果这样做了会抛出一个错误,表明该函数不是构造函数。因此,对于箭头函数,不存在可以作为构造函数调用的函数内部的new.target等绑定,相反,它们使用最接近的非箭头父函数的new.target值。

此外,由于无法使用new关键字调用箭头函数,因此实际上不需要它们具有原型。 因此,箭头函数不存在prototype属性。

由于箭头函数的prototypeundefined,尝试使用属性和方法来扩充它,或者访问它上面的属性,都会引发错误。

const Person (name = '小智') => {
  this.name = name
}
// 抛出错误
Person.prototype.getName = function() {
  return `我的名字是${this.name}`
}
// 抛出错误
const person = new Person()

console.log(Person.prototype)  // undefined
箭头函数中的 this

JS 函数的每次调用都与调用上下文相关联,这取决于函数是如何调用的,或者在哪里调用的。函数内部this值依赖于函数在调用时的调用上下文,这通常会让开发人员不得不问自己一个问题:this值是啥。

下面是对不同类型的函数调用this指向一些总结:

  • 使用new关键字调用:this指向由函数的内部[[Construct]]方法创建的新实例对象。this(新创建的实例对象)通常在默认情况下返回,除了在函数定义中显式指定了不同的返回值。

  • 不使用new关键字直接调用:在非严格模式下,this指向window对象(浏览器中)。然而,在严格模式下,this值为undefined,因此,试图访问或设置 this属性将引发错误。

  • 间接使用绑定对象调用:Function.prototype对象提供了三种方法,可以在调用函数时将函数绑定到任意对象,即:call()apply()bind()。 使用这些方法调用函数时,this指向指定的绑定对象。

  • 作为对象方法调用:this指向调用函数(方法)的对象,无论该方法是被定义为对象的自己的属性还是从对象的原型链中解析。

  • 作为事件处理程序调用:对于用作 DOM 事件侦听器的常规函数,this指向触发事件的目标对象、DOM元素、documentwindow

function processFormData (evt) {
  evt.preventDefault()

  const form = this.closest('form')

  const data = new FormData(form)
  const { action: url, method } = form
}

button.addEventListener('click', processFormData, false)

与前面看到的一样,事件侦听器函数中的 this值是触发单击事件的 DOM 元素,在本例中是 button。因此,可以使用以下命令指向submit按钮的父表单:

this.closest('form')

如果将函数更改为箭头函数语法,会发生什么?

const processFormData = (evt) => {
  evt.preventDefault()

  const form = this.closest('form')
  const data = new FormData(form)
  const { action: url, method } = form
}

button.addEventListener('click', processFormData, false)

如果现在尝试此操作,咱们就得到一个错误。从表面上看,this 的值并不是各位想要的。由于某种原因,它不再指向button元素,而是指向window对象。

如何修复this指向

利用上面提到的 Function.prototype.bind() 强制将this值绑定到button元素:

button.addEventListener('click', processFormData.bind(button), false);

但这似乎不是各位想要的解决办法。this仍然指向window对象,这是箭头函数特有的问题吗?这是否意味着箭头函数不能用于依赖于this的事件处理?

关于箭头函数的最后一件事:

与常规函数不同,箭头函数没有this的绑定。 this的值将解析为最接近的非箭头父函数或全局对象的值。

这解释了为什么事件侦听器箭头函数中的this值指向 window 对象(全局对象)。 由于它没有嵌套在父函数中,因此它使用来自最近的父作用域的this值,该作用域是全局作用域。

但是,这并不能解释为什么不能使用bind()将事件侦听器箭头函数绑定到button元素。对此有一个解释:

与常规函数不同,内部箭头函数的this值保持不变,并且无论调用上下文如何,都不能在其整个生命周期中更改。

箭头函数的这种行为使得JS引擎可以优化它们,因为可以事先确定函数绑定。

考虑一个稍微不同的场景,其中事件处理程序是使用对象方法中的常规函数​​定义的,并且还取决于同一对象的另一个方法:

({
  _sortByFileSize: function (filelist) {
    const files = Array.from(filelist).sort(function (a, b) {
      return a.size - b.size;
    });

    return files.map(function (file) {
      return file.name;
    });
  },

  init: function (input) {
    input.addEventListener('change', function (evt) {
      const files = evt.target.files;
      console.log(this._sortByFileSize(files));
    }, false);
  }

}).init(document.getElementById('file-input'));

上面是一个一次性的对象,该对象带有_sortByFileSize()方法和init()方法,并立即调init方法。init()方法接受一个input元素,并为input元素设置一个更改事件处理程序,该事件处理程序按文件大小对上传的文件进行排序,并打印在浏览器的控制台。

如果测试这段代码,会发现,当选择要上载的文件时,文件列表不会被排序并打印到控制台;相反,会控制台上抛出一个错误,问题就出在这一行:

console.log(this._sortByFileSize(files));

在事件监听器函数内部,this 指向 input 元素 因此this._sortByFileSizeundefined

要解决此问题,需要将事件侦听器中的this绑定到包含方法的外部对象,以便可以调用this._sortByFileSize()。 在这里,可以使用bind(),如下所示:

init: function (input) {
  input.addEventListener('change', (function (evt) {
    const files = evt.target.files;
    console.log(this._sortByFileSize(files));
  }).bind(this), false);
}

现在一切正常。这里不使用bind(),可以简单地用一个箭头函数替换事件侦听器函数。箭头函数将使用父init()方法中的this的值:

init: function (input) {
  input.addEventListener('change', (function (evt) {
    const files = evt.target.files;
    console.log(this._sortByFileSize(files));
  }).bind(this), false);
}

再考虑一个场景,假设有一个简单的计时器函数,可以将其作为构造函数调用来创建以秒为单位的倒计时计时器。使用setInterval()进行倒计时,直到持续时间过期或间隔被清除为止,如下所示:

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval(function () {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }, 1000);
}

const timer = new Timer(30);

如果运行这段代码,会看到倒计时计时器似乎被打破了,在控制台上一直打印 NaN

这里的问题是,在传递给setInterval()的回调函数中,this指向全局window对象,而不是Timer()函数作用域内新创建的实例对象。因此,this.secondsthis.interval 都是undefined的。

与之前一样,要修复这个问题,可以使用bind()setInterval()回调函数中的this值绑定到新创建的实例对象,如下所示:

function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval((function () {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }).bind(this), 1000);
}

或者,更好的方法是,可以用一个箭头函数替换setInterval()回调函数,这样它就可以使用最近的非箭头父函数的this值:


function Timer (seconds = 60) {
  this.seconds = parseInt(seconds) || 60;
  console.log(this.seconds);

  this.interval = setInterval(() => {
    console.log(--this.seconds);

    if (this.seconds == 0) {
      this.interval && clearInterval(this.interval);
    }
  }, 1000);
}

现在理解了箭头函数如何处理this关键字,还需要注意箭头函数对于需要保留this值的情况并不理想 - 例如,在定义需要引用的对象方法时 使用需要引用目标对象的方法来扩展对象或扩充函数的原型。

不存在的绑定

在本文中,已经看到了一些绑定,这些绑定可以在常规JS函数中使用,但是不存在用于箭头函数的绑定。相反,箭头函数从最近的非箭头父函数派生此类绑定的值。

总之,下面是箭头函数中不存在绑定的列表:

  • arguments:调用时传递给函数的参数列表
  • new.target:使用new关键字作为构造函数调用的函数的引用
  • super:对函数所属对象原型的引用,前提是该对象被定义为一个简洁的对象方法
  • this:对函数的调用上下文对象的引用

什么时候不使用箭头函数

前面咱们描述了箭头函数的上下文透明性和简短的语法,但凡事都有两面,有些情况下是不适合使用箭头函数的,这节咱们来讲讲有哪些情况下不能使用箭头函数。

定义对象上的方法

在JS中,方法是存储在对象属性中的函数。当调用该方法时,this 将指向该方法所属的对象。由于箭头函数语法简短,所以使用它来定义方法是很有吸引力的,让咱们来试一试:

const calculate = {
  array: [1, 2, 3],
  sum: () => {
    console.log(this === window) // => true
    return this.array.reduce((result, item) => result + item) 
  }
};
console.log(this === window) // => true
// Throws "TypeError: Cannot read property 'reduce' of undefined"
calculate.sum();

calculate.sum方法用箭头函数定义。 但是在调用时,calculate.sum() 会抛出一个TypeError,因为this.arrayundefined

当调用calculate对象上的方法sum()时,上下文仍然是 window。之所以会发生这种情况,是因为箭头函数按词法作用域将上下文绑定到 window 对象,执行 this.array等同于window.array,它是undefined

解决方法是使用常规函数表达式来定义方法。 this 是在调用时确定的,而不是由封闭的上下文决定的,来看看修复后的版本:

const calculate = {  
  array: [1, 2, 3],
  sum() {
    console.log(this === calculate); // => true
    return this.array.reduce((result, item) => result + item);
  }
};
calculate.sum(); // => 6

因为sum是常规函数,所以在调用 calculate.sum()thiscalculate 对象。 this.array是数组引用,因此正确计算元素之和:6

同样的规则也适用于在原型对象上定义方法。使用一个箭头函数来定义getName方法,this 指向 window:

function Person(name) {
  this.name = name
}
Person.prototype.getName = () => {
  console.log(this === window) // => true
  return this.name
};
const person = new Person('小智')
person.getName() // => undefined

使用早期的方式定义函数表达式:

function Person(name) {
  this.name = name;
}
Person.prototype.getName = function() {
  console.log(this === person) // => true
  return this.name;
};
const person = new Person('小智')
person.getName() // => '小智'

getName 常规函数在作为方法调用时将上下文更改为 person 对象:person.getName()

#### 2. 动态上下文的回调函数

`this` 在 JS 中是一个强大的特性,它允许根据调用函数的方式更改上下文。通常,上下文是调用发生的目标对象,这使得代码更加自然,就像这个对象发生了什么。

但是,箭头函数会在声明上静态绑定上下文,并且无法使其动态化,但这种方式有坏也有好,有时候我们需要动态绑定。在客户端编程中,将事件侦听器附加到 DOM 元素是一项常见的任务。事件触发处理程序函数,并将this作为目标元素,这里如果使用箭头函数就不够灵活。

下面的示例尝试为这样的处理程序使用箭头函数:

const button = document.getElementById('myButton')
button.addEventListener('click', () => {
  console.log(this === window) // => true
  this.innerHTML = 'Clicked button'
})

在全局上下文中 this 指向 window。 当发生单击事件时,浏览器尝试使用按钮上下文调用处理函数,但箭头函数不会更改其预定义的上下文。this.innerHTML相当于window.innerHTML,没有任何意义。

必须应用函数表达式,该表达式允许根据目标元素更改 this

const button = document.getElementById('myButton');
button.addEventListener('click', function() {
  console.log(this === button) // => true
  this.innerHTML = 'Clicked button'
})

当用户单击按钮时,处理程序函数中的 this 指向 button。因此 this.innerHTML = 'Clicked button' 正确地修改按钮文本以反映已单击状态。

3.调用构造函数

this 在构造调用中是新创建的对象。当执行new MyFunction()时,构造函数MyFunction的上下文是一个新对象:this instanceof MyFunction === true

注意,箭头函数不能用作构造函数。 JS 通过抛出异常隐式阻止这样做。

无论如何,this是来自封闭上下文的设置,而不是新创建的对象。换句话说,箭头函数构造函数调用没有意义,而且是模糊的。

让我们看看如果尝试这样做会发生什么:

const Message = (text) => {
  this.text = text;
}
// Throws "TypeError: Message is not a constructor"
const helloMessage = new Message('Hello World!')

执行new Message('Hello World!'),其中Message是一个箭头函数,JS 抛出一个 TypeError 错误,Message不能用作构造函数。

上面的例子可以使用函数表达式来修复,这是创建构造函数的正确方法(包括函数声明):

const Message = function(text) {
  this.text = text
}
const helloMessage = new Message('Hello World!')

总结

毫无疑问,箭头函数是一个很好的补充。当正确使用时,它会使前面必须使用.bind()或试图捕获上下文的地方变得简单,它还简化了代码。

某些情况下的优点会给其他情况带来不利。 当需要动态上下文时,不能使用箭头函数:定义方法,使用构造函数创建对象,在处理事件时从 this 获取目标。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

@大迁世界

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值