目录:
- 参数默认值
- ES6之前我们给函数参数默认值的方式
- 使用更加舒服的ES6参数默认值
- 【 扩展 】ES6参数默认值对arguments的影响
- 【 扩展 】参数默认值和暂时性死区
- 剩余参数
- ES6之前处理不限定参数函数的方法
- 剩余参数运算符
- new.target
- ES5检测是否通过new操作符使用构造函数的方法
- ES6的解决方案
- 箭头函数
- ES5关于函数this指向的一些烦恼和骚操作
- 箭头函数混脸熟
- 箭头函数特点
- 箭头函数的最佳实践
- 【 扩展 】关于this指向问题
参数默认值
在ES6之前我们是怎么给参数默认值的?
function foo( a, b ) {
// 为什么不能写 a = a || 1; b = b || 2, 因为一旦用户传递0进来就也会走进默认值, 这是说不通的
a = a === undefined ? 1 : a;
b = b === undefined ? 2 : b;
return a + b;
}
console.log(foo(2, 3)); // 输出5
console.log(foo(6)); // 输出8
上面这样给默认值, 其实让我们的代码量增加了这个是小事, 主要是也不利于阅读和维护。特别是在参数一多的情况下
使用更加舒服的ES6参数默认值
在书写形参的时候, 直接给形参以赋值的形式设置默认值, 当函数调用的时候, 如果传递的相应实参为undefined, 系统将会使用默认值
// a = 1, b = 2就是给了默认值了
function foo( a = 1, b = 2) {
return a + b;
}
console.log(foo(2, 3)); // 输出5
console.log(foo(6)); // 输出8
有了这个参数默认值以后, 我们阅读代码其实更加方便了, 书写起来也不用这么繁琐了, 这是参数默认值带给我们最直接的好处
【 扩展 】ES6参数默认值对arguments的影响
在ES5中, 我们知道arguments列表和形参列表是相互映射的, 你变我也变得那种, 但是在开启严格模式下失效, 同样当我们使用ES6的参数默认值语法时, arguments将不再与形参列表相互映射
function test( a, b ) {
console.log( a, b ); // 输出1, 2
console.log( arguments.a, arguments.b ); // 输出1, 2
a = 10;
arguments.b = 30;
console.log( a, b ); // 输出10, 30
console.log( arguments.a, arguments.b ); // 输出10, 30
}
test(1, 2);
function foo( a = 2, b = 3 ) {
console.log( a, b ); // 输出1, 2
console.log( arguments.a, arguments.b ); // 输出1, 2
a = 10;
arguments.b = 30;
console.log( a, b ); // 输出10, 2
console.log( arguments.a, arguments.b ); // 1, 30
}
foo(1, 2)
"use strict"
function demo( a, b ) {
console.log( a, b ); // 输出1, 2
console.log( arguments.a, arguments.b ); // 输出1, 2
a = 10;
arguments.b = 30;
console.log( a, b ); // 输出10, 2
console.log( arguments.a, arguments.b ); // 1, 30
}
demo(1, 2);
【 扩展 】参数默认值和暂时性死区
只要我们给了形参默认值的语法, 形参将会和let, const一样开启暂时性死区
function demo( a, b = a ) {
return a + b;
}
console.log(demo(1)); // 输出2
function foo( a = b, b ) {
return a + b;
}
console.log(foo(1, 2)); // 直接报错, 因为在b还没有声明之前就使用了b
剩余参数
ES5处理不限定参数函数的方法
假设有一个累加函数, 累加函数的参数是没办法限制的, 我们要让他可以传两个数字累加, 也可以10个, 可以100个, 所以在ES6以前我们往往这么处理
- 强制调用函数者传递数组
- 使用arguments
// 1. 强制调用者传递数组
function getSumByArr( numberArr ) {
let sumNumber = 0;
numberArr.forEach(function(it) {
sumNumber += it;
})
return sumNumber;
}
console.log(getSumByArr([1, 2, 3])); // 输出6
// 2. 使用arguments
function getSumByArg() {
let sumNumber = 0;
let args = [].slice.call(arguments);
args.forEach(function(it) {
sumNumber += it;
})
return sumNumber;
}
console.log(getSumByArg([1, 2, 3])); // 输出6
那么上面的两种方法有什么缺陷呢?
- 强制用户传递数组有一些不太方便
- 使用arguments会使得函数的可读性降低
- 在某些情况下, arguments的映射效果是一把双刃剑
ES6的剩余参数解决问题
ES6的剩余参数语法专门用于收集函数末尾的参数, 并将其放进一个形参数组中
语法如下:
function foo(...形参数组名) {}
我们来看看这哥们做的事情吧, 同样实现一个求和函数
function getSum(...numberArr) {
let sumNumber = 0;
numberArr.forEach(function(it) {
sumNumber += it;
})
return sumNumber;
}
console.log(getSum(1, 2, 3)); // 输出6
console.log(getSum(2, 3, 4, 5)); // 输出14
这样我们不仅在阅读上能够比较清晰的明白getSum函数会接收一系列参数, 也不用担心arguments带来的部分问题啦
剩余参数需要注意的事情
- 剩余参数必须是函数的最后一个形参, 因为如果不是最后一个的话, 你叫人家咋收啊
function demo( x, y, ...arg) {
console.log(x, y, arg);
}
demo( 1, 2, 3, 4); // 这样调用系统知道x = 1, y = 2, 然后arg将3 和 4收进一个数组
function demo( x, ...arg, y ) {
console.log(x, y, arg);
}
demo( 1, 2, 3, 4, 5 ); // 这你告诉人家...arg要怎么个收法啊, 他知道arg要收哪些变量? 所以我们不能将剩余参数放在中间, 必须在最后一个参数
- 一个函数仅能出现一个剩余参数
function demo(...arg) {
// 他知道把所有的参数都放进arg里
}
demo( 1, 2, 3 );
function foo(...arg1, ...arg2) {
// 你说这咋分啊, arg1拿几个, arg2拿几个?
}
foo(2, 3, 4);
// 所以一个函数仅可以出现一个剩余参数
new.target
ES5在判断构造函数是不是new的一些问题
一般来说, 我们定义构造函数就是要使用new实例对象的, 但是有时候不管是由于我们的粗心还是刻意不符合规范操作, 都会导致我们没有使用new但是构造函数并没有提示我们, 在过去我们是这样处理这个问题的, 如下
function Person( name, age ) {
if(!this instanceof Person) {
// 如果this的原型链上没有Person的原型, 则代表他并没有使用new
throw new Error('Person must be called with new ');
}
this.name = name;
this.age = age;
}
const lina = new Person( 'lina', 17 );
const dona = Person('dona', 18); // 这一行会直接报错, 因为如果不用new关键字, this一定指向window
const loki = Person.call(lina, 20); // 这样也不会报错, 因为使用call修改了this指向
上面最后的loki没有通过new但是也没有报错, 这很显然是ES5在判定是否有正确使用构造函数的缺陷
ES6的解决方案
ES6提供new.target属性来告知开发者调用构造函数的人是否是通过new调用的, 如果是则new.target会返回该构造函数, 如果不是则会返回undefined
function Person( name, age ) {
if(new.target === undefined) {
throw new Error('Person must be called with new ');
}
this.name = name;
this.age = age;
}
const lina = new Person( 'lina', 17 );
const dona = Person('dona', 18); // 这一行会直接报错, 因为如果不用new关键字, this一定指向window
const loki = Person.call(lina, 20); // 他也跑不掉, 会报错
箭头函数
过去我们解决嵌套函数this指向的一些骚操作
const tick = {
number: 10,
startTick: function() {
const _this = this; // 我们在setInterval的操作函数里是拿不到这个this的, 所以我们必须要保存一下
let timer = setInterval(function() {
_this.number --;
console.log(this.number);
if(_this.number === 0) {
clearInterval(timer);
}
}, 1000)
}
}
tick.startTick();
我们通常会像上面一样用一个新的变量来保存this, 或者我们会像下面一样使用bind函数来返回一个新的函数
const tick = {
number: 10,
startTick: function() {
let timer = setInterval(function() {
this.number --;
console.log(this.number);
if(this.number === 0) {
clearInterval(timer);
}
// 在这里使用bind绑定一下this
}.bind(this), 1000)
}
}
tick.startTick();
但是上面两种方案还是有一些问题的:
- 无论是使用_this保存变量还是使用bind返回一个新函数都造成了新变量的开销, 如果页面中有许多地方都有同样的操作, 那消耗的性能堆积起来也是个不小的消耗
- 如果有嵌套的情况, 代码会变得不那么好维护
ES6箭头函数登场
箭头函数是ES6推出的新的函数表达式书写方式, 理论上来说, 任何可以使用函数表达式和匿名函数的场景都可以写成箭头函数
语法如下:
(参数1, 参数2) => {
函数体
}
我们将上面的代码替换成函数表达式先瞅一瞅
const tick = {
number: 10,
startTick: function() {
let timer = setInterval(() => {
this.number --;
console.log(this.number);
if(this.number === 0) {
clearInterval(timer);
}
}, 1000)
}
}
tick.startTick();
我们会惊奇的发现, 我们一没有在外面声明this的变量, 也没有使用bind, 但是使用箭头函数竟然可以直接让代码不报错。
别急, 这只是让你混个脸熟, 接下来我们好好看看这个箭头函数的用法和特点
箭头函数的特点
-
箭头函数中的this指向有两种说法, 这两种说法都对, 但是本质上是第二种说法, 第一种说法只是第二种说法的一种现象
- 箭头函数的this指向取决于离他最近的非箭头函数父级的this指向, 与如何调用无关 如果无非箭头函数父级, 则为window
- 箭头函数的this指向取决于箭头函数定义的环境的this指向, 与如何调用无关
-
箭头函数的参数如果只有一个, 则可以省略掉小括号, 直接书写参数( 没有参数或者多个参数都不满足要求 )
const demo = (config) => {
console.log(config);
}
// 上面的写法可以直接写成如下这样, 一个参数的情况下直接省略参数的小括号
const demo = config => {
console.log(config);
}
- 箭头函数如果只有一条返回值语句, 可以省略大括号和return关键字( 必须是只有一条返回语句 )
const demo = config => {
return config + 1;
}
// 上面的写法可以直接写成如下这样, 函数体只有一条返回语句的话直接可以省略大括号和return关键字
const demo = config => config + 1;
}
// 如果返回值是一个对象的话, 我们需要用小括号包裹一下对象, 来告诉系统我们这是返回值对象不是函数体括号
const demo = config => ({ a: config + 1 })
-
和this一样, 箭头函数中没有自己的arguments, new.target, 如果强行访问, 拿到的是箭头函数定义环境的arguments, new.target指向
-
箭头函数没有原型prototype
-
由于第五点, 所以箭头函数不能作为构造函数使用
箭头函数的最佳实践
-
适合箭头函数存在的场景:
- 事件处理函数( 如点击事件, 触摸事件 )
- 异步处理函数( 如计时器, ajax请求 )
- 其他临时使用一下的且不涉及this指向函数( 如一些通过参数累加或者做基本计算的工具函数 )
- 为了绑定外层this的函数( 如上面的那个例子 )
- 为了保持代码简洁的函数( 例如forEach, sort等, 这样做主要就是为了简洁 )
【 扩展 】关于this指向问题
- 谁调用的函数, this就指向谁
- 通过对象调用函数, this指向该对象
- 直接通过函数调用, this指向全局对象
- 通过new调用构造函数, this指向新创建的对象
- 通过apply, call, bind可以修改this指向
- 如果是dom事件函数, this指向事件源
- 箭头函数自身没有this指向, 依赖于创建时环境的this指向
- 在函数预编译过程中, this指向window