资讯专栏INFORMATION COLUMN

ES6系列---函数

AJie / 867人阅读

摘要:形参默认值不再影响对象当使用默认参数值时,对象的行为与以往有所不同。具有方法的函数被统称为构造函数。当调用函数的方法时,被赋值为新创建对象实例如果调用方法,则的值为。

由于JavaScript开发者多年的不断抱怨和呼吁,ES6终于大力度地更新了函数特性,在ES5基础上进行了许多改进。

函数形参的默认值 ES5形参默认值的实现

ES5中,你很可能通过以下这种方式为函数赋予默认值:

function makeRequest(url, timeout, callback) {
    timeout = timeout || 2000;
    callback = callback || function() {};
    
    // 函数的其余部分
}

对于函数的命名参数,如果不显式传值,则其默认值为undefined。因此经常是使用逻辑或操作符来为缺失的参数提供默认值。然而这个方式有缺陷,如果给第二个形参timeout传入值0,尽管这个值是合法的,也会被视为false值,对函数调用方来说,timeout非预期的被修改为2000。

更安全的选择是通过typeof检查参数类型,就像这样:

function makeRequest(url, timeout, callback) {
    timeout = (typeof timeout !== "undefined") ? timeout : 2000;
    callback = (typeof callback !== "undefined") ? callback : function() {};
    
    // 函数的其余部分
}

在流行的JavaScript库中均使用类似的模式进行默认值补全。

ES6形参默认值的实现

ES6简化了为形参提供默认值的过程,定义形参时即可指定初始值:

function makeRequest(url, timeout = 2000, callback) {
    // 函数的其余部分
}

这种情况下,只有当不为第二个参数传值或主动为第二个参数传入undefined时才会使用timeout的默认值,就像这样:

// 使用timeout的默认值
makeRequest("/foo", undefined, function(body) {
    doSomething(body);
});

// 使用timeout的默认值
makeRequest("/foo");

// 不使用timeout的默认值
makeRequest("foo", null, function(body){ // null是一个合法值
    doSomething(body);
});
ES6默认参数表达式

ES6中,关于默认参数值,还可以是非原始值传参,就像这样:

function getValue() {
    return 5;
}

function add(first, second = getValue()) {
    return first + second;
}

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

尤其注意,初次解析函数声明时是不会调用getValue()方法的,只有当调用add()函数且不传入第二个参数时才会被调用。另外,second = getValue()这里的小括号()不要忘记掉,不然最终传入的是对函数的引用,而不是函数调用的结果。

正因为默认参数是在函数调用时求值,所以可以使用先定义的参数作为后定义的参数的默认值,就像这样:

function add(first, second = first) {
    return first + second;
}

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

也可以将参数first传入一个函数来获得参数second的值,就像这样:

function getValue(value) {
    return value + 5;
}

function add(first, second = getValue(first)) {
    return first + second;
}

console.log(add(1, 1));  // 2
console.log(add(1));     // 7
默认参数的临时死区

在引用参数默认值的时,只允许引用前面参数的值,即先定义的参数不能访问后定义的参数:

function add(first = second, second) {
    return first + second;
}

console.log(add(1, 1));   // 2
console.log(add(undefined, 1));   // 抛出错误

在讲解letconst时介绍了临时死区(TDZ),其实默认参数也有同样的临时死区。上面这个示例,调用add(1, 1)和add(undefined, 1)相当于引擎在背后做了如下事情:

// 表示调用add(1, 1)时的JavaScript代码
let first = 1;
let second = 1;

// 表示调用add(undefined, 1)时的JavaScript代码
let first = second;
let second = 1;

这个示例中,调用add(undefined, 1)函数,因为当first初始化时second尚未初始化,所以会导致程序抛出错误,此时second(已声明,未初始化)尚处于临时死区中,正如讨论let绑定时所说的那样,任何引用临时死区中的绑定的行为都会报错。

函数参数有自己的作用域和临时死区,其与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量。

ES6形参默认值不再影响arguments对象

当使用默认参数值时,arguments对象的行为与以往有所不同。在ES5非严格模式下,函数命名参数的变化会体现在arguments对象中:

function mixArgs(first, second) {
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d";
    console.log(first === arguments[0]);
    consolo.log(second === arguments[1]);
}

mixArgs("a", "b");

这段代码会输出:

true
true
true
true

ES5非严格模式下,命名参数的变化会同步更新到arguments对象中,所以当firstsecond被赋予新值时,arguments[0]和arguments[1]相应地也就更新了,最终所有===全等结果都为true

ES5严格模式下,取消了arguments对象的这个令人感到困惑的行为,无论参数如何变化,arguments对象不再随之改变。ES6中,arguments对象的行为与ES5严格模式下保持一致。arguments对象保持与命名参数分离,这个微妙的细节将影响你使用arguments对象的方式,请看以下代码:

function mixArgs(first, second = "b") {
    console.log(arguments.length);
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
    first = "c";
    second = "d";
    console.log(first === arguments[0]);
    console.log(second === arguments[1]);
}

mixArgs("a");

这段代码会输出以下内容:

1
true
false
false
false
false

改变firstsecond并不会影响arguments对象。总是可以通过arguments对象将参数恢复为初始值。

处理无命名参数

JavaScript的函数语法规定,无论函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量。当传入更少数量的参数时,默认参数值的特性可以有效简化函数的声明;当传入更多数量的参数时,ES6同样也提供了更好的方案。

ES5中的无命名参数

早先,我们用JavaScript提供的arguments对象来检查函数的所有参数,从而不必定义每一个要用的参数,看下面的例子:

function pick(object) {
    let result = Object.create(null);
    
    // 从第二个参数开始
    for(let i = 1, len = arguments.length; i < len; i++) {
        result[arguments[i]] = object[arguments[i]];
    }
    
    return result;
}

let book = {
    title: "ECMAScript 6",
    author: "Nicholas",
    year: 2016
};

let bookData = pick(book, "author", "year");

console.log(bookData.author);   // "Nicholas"
console.log(bookData.year);     // 2016

这个函数模仿了Underscore.js库中的pick()方法,返回一个给定对象的副本,包含原始对象属性的特定子集。在这个示例中只定义了一个参数,第一个参数传入的是被复制的源对象,其他参数为被复制属性的名称。

关于pick()函数应该注意这样几件事:首先,并不容易发现这个函数可以接受任意数量的参数,当然,可以定义更多的参数,但是怎么也达不到要求;其次,因为第一个参数为命名参数并已被使用,当你要查找需要拷贝的属性名称时,不得不从索引1而不是索引0开始遍历arguments对象。牢记真正的索引位置并不难,但这总归是我们需要牵挂的问题。

ES6中,通过引入不定参数(rest parameters)的特性可以轻易解决这些问题。

ES6的不定参数

在函数的命名参数前添加三个点(...)就表明这是一个不定参数,该参数为一个数组,包含着自它之后传入的所有参数。使用不定参数重写pick()函数:

function pick(object, ...keys) {
    let result = Object.create(null);
    
    for(let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    
    return result;
}

只需要看一眼就知晓该函数可以处理的参数数量。
注意一下,函数的length属性统计的是函数命名参数的数量,不定参数的加入不会影响函数length属性。在本示例中pick()函数的length值为1,因为只会计算object。另外每个函数最多只能声明一个不定参数,并且一定要放在末尾。

不定参数对arguments对象的影响

不定参数的设计初衷是代替JavaScriptarguments对象的。但是arguments对象依然存在。如果声明函数时定义了不定参数,则在函数被调用时,arguments对象包含了所有传入函数的参数,就像这样:

function checkArgs(...args) {
    console.log(args.length);
    console.log(arguments.length);
    console.log(args[0], arguments[0]);
    console.log(args[1], arguments[1]);
}

checkArgs("a", "b");

调用checkArgs(),输出以下内容:

2
2
a a
b b

无论是否使用不定参数,arguments对象总是包含所有传入函数的参数。

展开运算符配合不定参数

展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。正好,不定参数希望的就是指定多个各自独立的参数,并通过整合后的数组来访问:

function max(...args) {
    // 计算并返回最大值
}

let values = [10, 20, 30, 40];
// 使用展开运算符打散
max(...values);
// 等价于
// max(10, 20, 30, 40)
构造函数

ES5及早期版本中的函数具有多重功能,可以结合new使用,函数内的this值将指向一个新对象,函数最终会返回这个新对象:

function Person(name) {
    this.name = name;
}

var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");

console.log(person);      // "[Object Object]"
console.log(notAPerson);  // "undefined"

JavaScript函数有两个不同的内部方法:[[Call]]和[[Construct]]。当通过new关键字调用函数时,执行的是[[Construct]]函数,它负责创建一个通常被称作实例的新对象,然后再执行函数体,将this绑定到实例上,并返回这个对象;如果不通过new关键字调用函数,则执行[[Call]]函数,从而直接执行代码中的函数体。具有[[Construct]]方法的函数被统称为构造函数。

不是所有函数都有[[Construct]]方法,因此不是所有函数都可以通过new来调用,比如ES6的箭头函数就没有这个[[Construct]]。

ES5判断函数是否用new调用

ES5中,如果想判断一个函数是否通过new关键字被调用,最流行的方式是使用instanceof

function Person(name) {
    if(this instanceof Person) {
        this.name = name;  // 如果通过new关键字调用
    } else {
        throw new Error("必须通过new关键字来调用Person。");
    }
}

var person = new Person("Nicholas");
var notAPerson = Person("Nicholas");  // 抛出错误

但是这个方法也不完全可靠,因为有一种不通过new关键字的方法也可以将this绑定到Person的实例上:

function Person(name) {
    if(this instanceof Person) {
        this.name = name;
    } else {
        throw new Error("必须通过new关键字来调用Person。");
    }
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");  // 有效!

调用Person.call()时将变量person传入作为第一个参数,相当于在Person函数里将this设为了person实例。对于函数本身,无法区分是通过Person.call()(或者是Person.apply())还是new关键字调用得到的Person的实例。

ES6判断函数是否用new调用

为了解决判断函数是否通过new关键字调用的问题,ES6引入了new.target这个元属性。当调用函数的[[Construct]]方法时,new.target被赋值为新创建对象实例;如果调用[[Call]]方法,则new.target的值为undefined

可以通过检查new.target是否被定义过来安全的检测一个函数是否是通过new关键字调用的,就像这样:

function Person(name) {
    // typeof new.target === Person 可以检查是否被某个特定的构造函所调用
    if(typeof new.target !== "undefined") {
        this.name = name;
    } else {
        throw new Error("必须通过new关键字来调用Person。");
    }
}

var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");  // 抛出错误!
箭头函数

ES6中,箭头函数是最有趣的新增特性。它与传统的JavaScript函数有些许不同,主要集中在以下方面:

没有this、super、arguments和new.target绑定
箭头函数中的this、super、arguments及new.target这些值由外围最近一层非箭头函数决定。

不能通过new关键字调用
箭头函数没有[[Construct]]方法,所以不能被用作构造函数,如果通过new关键字调用箭头函数,程序会抛出错误。

没有原型
由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性。

不可以改变this的绑定
函数内部的this值不可被改变,在函数的生命周期内始终保持一致。

不支持arguments对象
箭头函数没有arguments绑定,所以你必须通过命名参数和不定参数这两种形式访问函数的参数。

不支持重复的命名参数
箭头函数不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才不能有重复的命名参数。

this绑定是JavaScript程序中常见的错误来源,在函数内很容易就对this的值失去控制,其经常导致程序出现意想不到的行为,箭头函数消除了这方面的烦恼。

箭头函数的语法

单一参数:

let reflect = value => value;

// 相当于
let reflect = function(value) {
    return value;
};

两个以上参数:

let sum = (num1, num2) => num1 + num2;

// 相当于
let sum = function(num1, num2) {
    return num1 + num2;
};

没有参数:

let getName = () => "Nicholas";

// 相当于
let getName = function(){
    return "Nicholas";
};

多表达式组成的更传统的函数体:

let sum = (num1, num2) => {
    return num1 + num2;
};

// 相当于
let sum = function(num1, num2) {
    return num1 + num2;
};

空函数:

let doNothing = () => {};

// 相当于
let doNothing = function(){};

返回对象字面量:

let getTempItem = id => ({ id: id, name: "Temp" });

// 相当于
let getTempItem = function(id){
    return {
        id: id,
        name: "Temp"
    };
};

将对象字面量包裹在小括号中是为了将其与函数体区分开来。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/84960.html

相关文章

  • ES6 系列之 Babel 是如何编译 Class 的(上)

    摘要:前言在了解是如何编译前,我们先看看的和的构造函数是如何对应的。这是它跟普通构造函数的一个主要区别,后者不用也可以执行。该函数的作用就是将函数数组中的方法添加到构造函数或者构造函数的原型中,最后返回这个构造函数。 前言 在了解 Babel 是如何编译 class 前,我们先看看 ES6 的 class 和 ES5 的构造函数是如何对应的。毕竟,ES6 的 class 可以看作一个语法糖,...

    shadajin 评论0 收藏0
  • 使用JavaScript ES6的新特性计算Fibonacci(非波拉契数列)

    摘要:采用的生成非波拉契数列提供了原生的支持,语法非常有特色,关键字后面紧跟一个星号。的详细介绍参考官网先看如何用这个黑科技重新实现非波拉契树立的生成。在这个内部,我们定义了一个无限循环,用于计算非波拉契数列。 程序员面试系列 Java面试系列-webapp文件夹和WebContent文件夹的区别? 程序员面试系列:Spring MVC能响应HTTP请求的原因? Java程序员面试系列-什么...

    yanbingyun1990 评论0 收藏0
  • ES6 系列之箭头函数

    摘要:回顾我们先来回顾下箭头函数的基本语法。主要区别包括没有箭头函数没有,所以需要通过查找作用域链来确定的值。箭头函数并没有方法,不能被用作构造函数,如果通过的方式调用,会报错。 回顾 我们先来回顾下箭头函数的基本语法。 ES6 增加了箭头函数: let func = value => value; 相当于: let func = function (value) { return ...

    hsluoyz 评论0 收藏0
  • webpack4 系列教程(二): 编译 ES6

    摘要:今天介绍怎么编译的各种函数和语法。对于相关的匹配规则,除了匹配结尾的文件,还应该去除文件夹下的第三库的文件发布前已经被处理好了。它需要在我们项目的入口文件中被引入,或者在中配置。个人网站原文链接系列教程二编译 今天介绍webpack怎么编译ES6的各种函数和语法。敲黑板:这是webpack4版本哦, 有一些不同于webpack3的地方。 >>> 本节课源码 >>> 所有课程源码 1....

    graf 评论0 收藏0
  • ES6 系列之 Babel 是如何编译 Class 的(下)

    摘要:以上的代码对应到就是调用父类的值得注意的是关键字表示父类的构造函数,相当于的。举个例子这是因为作为构造函数的语法糖,同时有属性和属性,因此同时存在两条继承链。子类的属性,表示构造函数的继承,总是指向父类。 前言 在上一篇 《 ES6 系列 Babel 是如何编译 Class 的(上)》,我们知道了 Babel 是如何编译 Class 的,这篇我们学习 Babel 是如何用 ES5 实现...

    endiat 评论0 收藏0
  • ES6系列函数部分

    摘要:正是因为它没有,所以也就不能用作构造函数。不可以当作构造函数,也就是说,不可以使用命令,否则会抛出一个错误。不可以使用对象,该对象在函数体内不存在。 箭头函数 在之前ES5的版本中,我们定义一个函数的形式如下: function a() { // do something…… } 但是在ES6中,则新增了箭头函数的方式,ES6中允许使用箭头(=>)来定义函数。 () => { ...

    enda 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<