资讯专栏INFORMATION COLUMN

ECMAScript6(5):函数的扩展

afishhhhh / 3014人阅读

摘要:否则调用时依然需要传参报错注意这里不能用触发默认值这里我们还需要多带带讨论一下默认参数对的影响很明显,默认参数并不能加到中。关于作用域集中在函数扩展的最后讨论。那如果函数的默认参数是函数呢烧脑的要来了如果基础好那就根本谈不上不烧脑。

参数默认值

ES5中设置默认值非常不方便, 我们这样写:

</>复制代码

  1. function fun(a){
  2. a = a || 2;
  3. console.log(a);
  4. }
  5. fun(); //2
  6. fun(0); //2
  7. fun(1); //1

以上写法, 如果传入了参数, 但这个参数对应值的布尔型是 false, 就不起作用了。当然你也可以判断 arguments.length 是否为0来避免这个问题, 但每个函数这样写就太啰嗦了, 尤其参数比较多的时候。在 ES6 中我们可以直接写在参数表中, 如果实际调用传递了参数, 就用这个传过来的参数, 否则用默认参数。像这样:

</>复制代码

  1. function fun(a=2){
  2. console.log(a);
  3. }
  4. fun(); //2
  5. fun(0); //0
  6. fun(1); //1

其实函数默认参数这一点最强大的地方在于可以和解构赋值结合使用:

</>复制代码

  1. //参数传递
  2. function f([x, y, z=4]){
  3. return [x+1, y+2, z+3];
  4. }
  5. var [a, b, c] = f([1, 2]); //a=2, b=4, c=7
  6. [[1, 2], [3, 4]].map(([a, b]) => a + b); //返回 [3, 7]

通过上面这个例子不难发现, 不仅可以用解构的方法设置初始值, 还可以进行参数传递。当然, 这里也可以是对象形式的解构赋值。如果传入的参数无法解构, 就会报错:

</>复制代码

  1. function fun1({a=1, b=5, c="A"}){
  2. console.log(c + (a + b));
  3. }
  4. fun1({}); //"A6"
  5. fun1(); //TypeError, 因为无法解构
  6. //但这样设计函数对使用函数的码农很不友好
  7. //所以, 技巧:
  8. function fun2({a=1, b=5, c="A"}={}){
  9. console.log(c + (a + b));
  10. }
  11. fun2(); //"A6"

注意, 其实还有一种方法, 但不如这个好, 我们比较如下:

</>复制代码

  1. //fun1 比 fun2 好, 不会产生以外的 undefined
  2. function fun1({a=1, b=5, c="A"}={}){
  3. console.log(c + (a + b));
  4. }
  5. function fun2({a, b, c}={a: 1, b: 5, c: "A"}){
  6. console.log(c + (a + b));
  7. }
  8. //传了参数, 但没传全部参数就会出问题
  9. fun1({a: 8}); //"A13"
  10. fun2({a: 8}); //NaN

不过这里强烈建议, 将具有默认值的参数排在参数列表的后面。否则调用时依然需要传参:

</>复制代码

  1. function f1(a=1, b){
  2. console.log(a + b);
  3. }
  4. function f2(a, b=1){
  5. console.log(a + b);
  6. }
  7. f2(2); //3
  8. f1(, 2); //报错
  9. f1(undefined, 2); //3, 注意这里不能用 null 触发默认值

这里我们还需要多带带讨论一下默认参数对 arguments 的影响:

</>复制代码

  1. function foo(a = 1){
  2. console.log(a, arguments[0]);
  3. }
  4. foo(); //1 undefined
  5. foo(undefined); //1 undefined
  6. foo(2); //2 2
  7. foo(null); //null null

很明显,默认参数并不能加到 arguments 中。

函数的 length 属性

这个属性ES6 之前就是存在的, 记得length表示预计传入的形参个数, 也就是没有默认值的形参个数:

</>复制代码

  1. (function(a){}).length; //1
  2. (function(a = 5){}).length; //0
  3. (function(a, b, c=5){}).length; //2
  4. (function(...args){}).length; //0, rest参数也不计入 length
rest 参数

rest 参数形式为 ...变量名, 它会将对应的全部实际传递的变量放入数组中, 可以用它来替代 arguments:

</>复制代码

  1. function f(...val){
  2. console.log(val.join());
  3. }
  4. f(1, 2); //[1, 2]
  5. f(1, 2, 3, 4); //[1, 2, 3, 4]
  6. function g(a, ...val){
  7. console.log(val.join());
  8. }
  9. g(1, 2); //[2]
  10. g(1, 2, 3, 4); //[2, 3, 4]

否则这个函数 g 你的这样定义函数, 比较麻烦:

</>复制代码

  1. function g(a){
  2. console.log([].slice.call(arguments, 1).join());
  3. }

这里需要注意2点:

rest参数必须是函数的最后一个参数, 它的后面不能再定义参数, 否则会报错。

rest参数不计入函数的 length 属性中

建议:

所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。这样方便调用者以任何顺序传递参数。

不要在函数体内使用arguments变量,使用rest运算符(...)代替。因为rest运算符显式表明你想要获取参数,而且arguments是一个类似数组的对象,而rest运算符可以提供一个真正的数组。

使用默认值语法设置函数参数的默认值。

扩展运算符

扩展运算符类似 rest运算符的逆运算, 用 ... 表示, 放在一个(类)数组前, 将该数组展开成独立的元素序列:

</>复制代码

  1. console.log(1, ...[2, 3, 4], 5); //输出1, 2, 3, 4, 5

扩展运算符的用处很多:

可以用于快速改变类数组对象为数组对象, 也是用于其他可遍历对象:

</>复制代码

  1. [...document.querySelectorAll("li")]; //[
  2. ,
  3. ,
  4. ];

结合 rest 参数使函数事半功倍:

</>复制代码

  1. function push(arr, ...val){
  2. return arr.push(...val); //调用函数时, 将数组变为序列
  3. }

替代 apply 写法

</>复制代码

  1. var arr = [1, 2, 3];
  2. var max = Math.max(...arr); //3
  3. var arr2 = [4, 5, 6];
  4. arr.push(...arr2); //[1, 2, 3, 4, 5, 6]
  5. new Date(...[2013, 1, 1]); //ri Feb 01 2013 00: 00: 00 GMT+0800 (CST)

连接, 合并数组

</>复制代码

  1. var more = [4, 5];
  2. var arr = [1, 2, 3, ...more]; //[1, 2, 3, 4, 5]
  3. var a1 = [1, 2];
  4. var a2 = [3, 4];
  5. var a3 = [5, 6];
  6. var a = [...a1, ...a2, ...a3]; //[1, 2, 3, 4, 5, 6]

解构赋值

</>复制代码

  1. var a = [1, 2, 3, 4, 5];
  2. var [a1, ...more] = a; //a1 = 1, more = [2, 3, 4, 5]
  3. //注意, 扩展运算符必须放在解构赋值的结尾, 否则报错

字符串拆分

</>复制代码

  1. var str = "hello";
  2. var alpha = [...str]; //alpha = ["h", "e", "l", "l", "o"]
  3. [..."xuD83DuDE80y"].length; //3, 正确处理32位 unicode 字符

建议:使用扩展运算符(...)拷贝数组。

name 属性

name 属性返回函数的名字, 对于匿名函数返回空字符串。不过对于表达式法定义的函数, ES5 和 ES6有差别:

</>复制代码

  1. var fun = function(){}
  2. fun.name; //ES5: "", ES6: "fun"
  3. (function(){}).name; //""

对于有2个名字的函数, 返回后者, ES5 和 ES6没有差别:

</>复制代码

  1. var fun = function baz(){}
  2. fun.name; //baz

对于 Function 构造函数得到的函数, 返回 anonymous:

</>复制代码

  1. new Function("fun").name; //"anonymous"
  2. new Function().name; //"anonymous"
  3. (new Function).name; //"anonymous"

对于 bind 返回的函数, 加上 bound 前缀

</>复制代码

  1. function f(){}
  2. f.bind({}).name; //"bound f"
  3. (function(){}).bind({}).name; //"bound "
  4. (new Function).bind({}).name; //"bound anonymous"
箭头函数

箭头函数的形式如下:

</>复制代码

  1. var fun = (参数列表) => {函数体};

如果只有一个参数(且不指定默认值), 参数列表的圆括号可以省略; (如果没有参数, 圆括号不能省略)
如果只有一个 return 语句, 那么函数体的花括号也可以省略, 同时省略 return 关键字。

</>复制代码

  1. var fun = value => value + 1;
  2. //等同于
  3. var fun = function(value){
  4. return value + 1;
  5. }

</>复制代码

  1. var fun = () => 5;
  2. //等同于
  3. var fun = function(){
  4. return 5;
  5. }

如果箭头函数的参数或返回值有对象, 应该用 () 括起来:

</>复制代码

  1. var fun = n => ({name: n});
  2. var fun = ({num1=1, num2=3}={}) => num1 + num2;

看完之前的部分, 箭头函数应该不陌生了:

</>复制代码

  1. var warp = (...val) => val;
  2. var arr1 = warp(2, 1, 3); //[2, 1, 3]
  3. var arr2 = arr1.map(x => x * x); //[4, 1, 9]
  4. arr2.sort((a, b) => a - b); //[1, 4, 9]

使用箭头函数应注意以下几点:

不可以将函数当做构造函数调用, 即不能使用 new 命令;

不可以在箭头函数中使用 yield 返回值, 所以不能用过 Generator 函数;

函数体内不存在 arguments 参数;

函数体内部不构成独立的作用域, 内部的 this 和定义时候的上下文一致; 但可以通过 call, apply, bind 改变函数中的 this。关于作用域, 集中在ES6函数扩展的最后讨论。

举几个箭头函数的实例:
实例1: 实现功能如: insert(2).into([1, 3]).after(1)insert(2).into([1, 3]).before(3)这样的函数:

</>复制代码

  1. var insert = value => ({
  2. into: arr => ({
  3. before: val => {
  4. arr.splice(arr.indexOf(val), 0, value);
  5. return arr;
  6. },
  7. after: val => {
  8. arr.splice(arr.indexOf(val) + 1, 0, value);
  9. return arr;
  10. }
  11. })
  12. });
  13. console.log(insert(2).into([1, 3]).after(1));
  14. console.log(insert(2).into([1, 3]).before(3));

实例2: 构建一个管道(前一个函数的输出是后一个函数的输入):

</>复制代码

  1. var pipe = (...funcs) => (init_val) => funcs.reduce((a, b) => b(a), init_val);
  2. //实现 2 的 (3+2) 次方
  3. var plus = a => a + 2;
  4. pipe(plus, Math.pow.bind(null, 2))(3); //32

实例3: 实现 λ 演算

</>复制代码

  1. //fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))
  2. var fix = f => (x => f(v => x(x)(v)))(x => f(v => x(x)(v)));

建议:箭头函数取代 Function.prototype.bind,不应再用 self / _this / that 绑定 this。其次,简单的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。

这里需要强调,以下情况不能使用箭头函数:

定义字面量方法

</>复制代码

  1. let calculator = {
  2. array: [1, 2, 3],
  3. sum: () => {
  4. return this.array.reduce((result, item) => result + item); //这里的 this 成了 window
  5. }
  6. };
  7. calculator.sum(); //"TypeError: Cannot read property "reduce" of undefined"

定义原型方法

</>复制代码

  1. function Cat(name) {
  2. this.name = name;
  3. }
  4. Cat.prototype.sayCatName = () => {
  5. return this.name; //和上一个问题一样:这里的 this 成了 window
  6. };
  7. let cat = new Cat("Mew");
  8. cat.sayCatName(); //undefined

绑定事件

</>复制代码

  1. const button = document.getElementById("myButton");
  2. button.addEventListener("click", () => {
  3. this.innerHTML = "Clicked button"; //这里的 this 本应该是 button, 但不幸的成了 window
  4. });

定义构造函数

</>复制代码

  1. let Message = (text) => {
  2. this.text = text;
  3. };
  4. let helloMessage = new Message("Hello World!"); //TypeError: Message is not a constructor

不要为了追求代码的简短丧失可读性

</>复制代码

  1. let multiply = (a, b) => b === undefined ? b => a * b : a * b; //这个太难读了,太费时间
  2. let double = multiply(2);
  3. double(3); //6
  4. multiply(2, 3); //6
函数绑定

ES7 中提出了函数绑定运算, 免去我们使用 call, bind, apply 的各种不方便, 形式如下:

</>复制代码

  1. objName::funcName

以下几组语句两两等同

</>复制代码

  1. var newFunc = obj::func;
  2. //相当于
  3. var newFunc = func.bind(obj);
  4. var result = obj::func(...arguments);
  5. //相当于
  6. var result = func.apply(obj, arguments);

如果 :: 左边的对象原本就是右边方法中的 this, 左边可以省略

</>复制代码

  1. var fun = obj::obj.func;
  2. //相当于
  3. var fun = ::obj.func;
  4. //相当于
  5. var fun = obj.func.bind(obj);

:: 运算返回的还是对象, 可以进行链式调用:

</>复制代码

  1. $(".my-class")::find("p")::text("new text");
  2. //相当于
  3. $(".my-class").find("p").text("new text");
尾调用优化

尾调用是函数式编程的概念, 指在函数最后调用另一个函数。

</>复制代码

  1. //是尾调用
  2. function a(){
  3. return g();
  4. }
  5. function b(p){
  6. if(p>0){
  7. return m();
  8. }
  9. return n();
  10. }
  11. function c(){
  12. return c();
  13. }
  14. //以下不是尾调用
  15. function d(){
  16. var b1 = g();
  17. return b1;
  18. }
  19. function e(){
  20. g();
  21. }
  22. function f(){
  23. return g() + 1;
  24. }

尾调用的一个显著特点就是, 我们可以将函数尾部调用的函数放在该函数外面(后面), 而不改变程序实现结果。这样可以减少函数调用栈的开销。
这样的优化在 ES6 的严格模式中被强制实现了, 我们需要做的仅仅是在使用时候利用好这个优化特性, 比如下面这个阶乘函数:

</>复制代码

  1. function factorial(n){
  2. if(n <= 1) return 1;
  3. return n * factorial(n - 1);
  4. }
  5. factorial(5); //120

这个函数计算 n 的阶乘, 就要在内存保留 n 个函数调用记录, 空间复杂度 O(n), 如果 n 很大可能会溢出。所以进行优化如下:

</>复制代码

  1. "use strict";
  2. function factorial(n, result = 1){
  3. if(n <= 1) return result;
  4. return factorial(n - 1, n * result);
  5. }
  6. factorial(5); //120

当然也可以使用柯里化:

</>复制代码

  1. var factorial = (function factor(result, n){
  2. if(n <= 1) return result;
  3. return factor(n * result, n - 1);
  4. }).bind(null, 1);
  5. factorial(5); //120
函数的尾逗号

这个仅仅是一个提案: 为了更好地进行版本控制, 在函数参数尾部加一个逗号, 表示该函数日后会被修改, 便于版本控制器跟踪。目前并未实现。

作用域

这里仅仅讨论 ES6 中的变量作用域。除了 let 和 const 定义的的变量具有块级作用域以外, varfunction 依旧遵守词法作用域, 词法作用域可以参考博主的另一篇文章javascript函数、作用域链与闭包

首先看一个例子:

</>复制代码

  1. var x = 1;
  2. function f(x, y=x){
  3. console.log(y);
  4. }
  5. f(2); //2

这个例子输出了2, 因为 y 在初始化的时候, 函数内部的 x 已经定义并完成赋值了, 所以, y = x 中的 x 已经是函数的局部变量 x 了, 而不是全局的 x。当然, 如果局部 x 变量在 y 声明之后声明就没问题了。

</>复制代码

  1. var x = 1;
  2. function f(y=x){
  3. let x = 2
  4. console.log(y);
  5. }
  6. f(); //1

那如果函数的默认参数是函数呢?烧脑的要来了:

</>复制代码

  1. var foo = "outer";
  2. function f(x){
  3. return foo;
  4. }
  5. function fun(foo, func = f){
  6. console.log(func());
  7. }
  8. fun("inner"); //"outer"

如果基础好, 那就根本谈不上不烧脑。因为, 函数中的作用域取决于函数定义的地方, 函数中的 this 取决于函数调用的方式。(敲黑板)
但如果这样写, 就是 inner 了, 因为func默认函数定义的时候 fun内的 foo 已经存在了。

</>复制代码

  1. var foo = "outer";
  2. function fun(foo, func = function(x){
  3. return foo;
  4. }){
  5. console.log(func());
  6. }
  7. fun("inner"); //"inner"

技巧: 利用默认值保证必需的参数被传入, 而减少对参数存在性的验证:

</>复制代码

  1. function throwErr(){
  2. throw new Error("Missing Parameter");
  3. }
  4. function fun(necessary = throwErr()){
  5. //...如果参数necessary没有收到就使用参数, 从而执行函数抛出错误
  6. }
  7. //当然也可以这样表示一个参数是可选的
  8. function fun(optional = undefined){
  9. //...
  10. }

箭头函数的作用域和定义时的上下文一致, 但可以通过调用方式改变:

</>复制代码

  1. window && (window.name = "global") || (global.name = "global");
  2. var o = {
  3. name: "obj-o",
  4. foo: function (){
  5. setTimeout(() => {console.log(this.name); }, 500);
  6. }
  7. }
  8. var p = {
  9. name: "obj-p",
  10. foo: function (){
  11. setTimeout(function(){console.log(this.name); }, 1000);
  12. }
  13. }
  14. o.foo(); //"obj-o"
  15. p.foo(); //"global"
  16. var temp = {
  17. name: "obj-temp"
  18. }
  19. o.foo.bind(temp)(); //"obj-temp"
  20. o.foo.call(temp); //"obj-temp"
  21. o.foo.apply(temp); //"obj-temp"
  22. p.foo.bind(temp)(); //"global"
  23. p.foo.call(temp); //"global"
  24. p.foo.apply(temp); //"global"

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

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

相关文章

  • ECMAScript6(6):数组扩展

    摘要:数组的扩展将类数组对象和可遍历对象转化为真正的数组。这两个函数的参数都是回调函数。遍历数组找到符合条件回调函数返回为的第一个值返回其值返回其下标。这三个方法用来遍历数组返回一个遍历器供使用其中是对键的遍历是对值的遍历是对键值对的遍历。 数组的扩展 Array, from() 将类数组对象和可遍历对象转化为真正的数组。 var arrayLike = { 0 : a, 1 : b...

    DrizzleX 评论0 收藏0
  • ECMAScript6 新特性——“数组扩展

    摘要:原来的也被修改了数组实例的喝方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为的成员,然后返回该成员。数组实例的方法使用给定值,填充一个数组。 1 Array.from() Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括...

    Eminjannn 评论0 收藏0
  • ECMAScript6 新特性——“函数扩展

    摘要:返回空字符串,返回将一个具名函数赋值给一个变量,则和的属性都返回这个具名函数原本的名字。不可以使用对象,该对象在函数体内不存在。等到运行结束,将结果返回到,的调用帧才会消失。 1 函数参数的默认值 ES6允许为函数的参数设置默认值,即直接写在参数定义的后面: function log(x = message.,y = duration infomation.) { consol...

    Jiavan 评论0 收藏0
  • ECMAScript6 新特性——“正则扩展

    摘要:第二个参数指定修饰符,如果存在则使用指定的修饰符。属性表示是否设置了修饰符属性的属性返回正则表达式的正文的属性返回正则表达式的修饰符字符串必须转义,才能作为正则模式。 1 RegExp构造函数 ES6 允许RegExp构造函数接受正则表达式作为参数。第二个参数指定修饰符,如果存在则使用指定的修饰符。 var regexp = new RegExp(/xyz/i, ig); consol...

    Shisui 评论0 收藏0
  • ECMAScript6 新特性——“对象扩展

    摘要:属性的简洁表示法允许直接写入变量和函数作为对象的属性和方法。,中有返回一个数组,成员是参数对象自身的不含继承的所有可遍历属性的键名。对象的扩展运算符目前,有一个提案,将解构赋值扩展运算符引入对象。 1 属性的简洁表示法 ES6允许直接写入变量和函数作为对象的属性和方法。 写入属性 var name = value; var obj = { name }; console.log...

    Clect 评论0 收藏0
  • ECMAScript6(9):正则表达式扩展

    摘要:正则表达式扩展构造函数支持传入正则得到拷贝,同时可以用第二参修改修饰符引入新的修饰符中的修饰符有个加上的修饰符,一共个修饰符描述描述多行模式忽略大小写模式全局匹配模式模式粘连模式模式为了兼容自己我们需要在一下情况使用该模式情况很明显这个是不 正则表达式扩展 构造函数支持传入正则得到拷贝,同时可以用第二参修改修饰符 var reg = /^abc/ig; var newReg_ig = ...

    Donne 评论0 收藏0

发表评论

0条评论

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