ES6笔记( 三 )- Function

目录:

  1. 参数默认值
    • ES6之前我们给函数参数默认值的方式
    • 使用更加舒服的ES6参数默认值
    • 【 扩展 】ES6参数默认值对arguments的影响
    • 【 扩展 】参数默认值和暂时性死区
  2. 剩余参数
    • ES6之前处理不限定参数函数的方法
    • 剩余参数运算符
  3. new.target
    • ES5检测是否通过new操作符使用构造函数的方法
    • ES6的解决方案
  4. 箭头函数
    • 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以前我们往往这么处理

  1. 强制调用函数者传递数组
  2. 使用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

那么上面的两种方法有什么缺陷呢?

  1. 强制用户传递数组有一些不太方便
  2. 使用arguments会使得函数的可读性降低
  3. 在某些情况下, 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();

但是上面两种方案还是有一些问题的:

  1. 无论是使用_this保存变量还是使用bind返回一个新函数都造成了新变量的开销, 如果页面中有许多地方都有同样的操作, 那消耗的性能堆积起来也是个不小的消耗
  2. 如果有嵌套的情况, 代码会变得不那么好维护

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, 但是使用箭头函数竟然可以直接让代码不报错。

别急, 这只是让你混个脸熟, 接下来我们好好看看这个箭头函数的用法和特点

箭头函数的特点

  1. 箭头函数中的this指向有两种说法, 这两种说法都对, 但是本质上是第二种说法, 第一种说法只是第二种说法的一种现象

    1. 箭头函数的this指向取决于离他最近的非箭头函数父级的this指向, 与如何调用无关 如果无非箭头函数父级, 则为window
    2. 箭头函数的this指向取决于箭头函数定义的环境的this指向, 与如何调用无关
  2. 箭头函数的参数如果只有一个, 则可以省略掉小括号, 直接书写参数( 没有参数或者多个参数都不满足要求 )

const demo = (config) => {
    console.log(config);
}

// 上面的写法可以直接写成如下这样, 一个参数的情况下直接省略参数的小括号
const demo = config => {
    console.log(config);
}
  1. 箭头函数如果只有一条返回值语句, 可以省略大括号和return关键字( 必须是只有一条返回语句 )
const demo = config => {
    return config + 1;
}

// 上面的写法可以直接写成如下这样, 函数体只有一条返回语句的话直接可以省略大括号和return关键字
const demo = config => config + 1;
}

// 如果返回值是一个对象的话, 我们需要用小括号包裹一下对象, 来告诉系统我们这是返回值对象不是函数体括号
const demo = config => ({ a: config + 1 })
  1. 和this一样, 箭头函数中没有自己的arguments, new.target, 如果强行访问, 拿到的是箭头函数定义环境的arguments, new.target指向

  2. 箭头函数没有原型prototype

  3. 由于第五点, 所以箭头函数不能作为构造函数使用

箭头函数的最佳实践

  1. 适合箭头函数存在的场景:

    • 事件处理函数( 如点击事件, 触摸事件 )
    • 异步处理函数( 如计时器, ajax请求 )
    • 其他临时使用一下的且不涉及this指向函数( 如一些通过参数累加或者做基本计算的工具函数 )
    • 为了绑定外层this的函数( 如上面的那个例子 )
    • 为了保持代码简洁的函数( 例如forEach, sort等, 这样做主要就是为了简洁 )

【 扩展 】关于this指向问题

  1. 谁调用的函数, this就指向谁
    • 通过对象调用函数, this指向该对象
    • 直接通过函数调用, this指向全局对象
  2. 通过new调用构造函数, this指向新创建的对象
  3. 通过apply, call, bind可以修改this指向
  4. 如果是dom事件函数, this指向事件源
  5. 箭头函数自身没有this指向, 依赖于创建时环境的this指向
  6. 在函数预编译过程中, this指向window

猜你喜欢

转载自blog.csdn.net/weixin_44238796/article/details/107584150
今日推荐