zoukankan      html  css  js  c++  java
  • ES6类型扩展函数扩展

    形参默认值

    JS中的函数,无论在函数定义时声明了多少形参,在函数调用时都可以传入任意数量的参数。

    通常定义函数时会为可选的参数定义默认值,这样可以更方便的针对参数数量添加处理逻辑。

    ES6为函数形参定义默认值很简单,直接在形参后面添加默认值即可

    function foo(url, timeout = 3000, callback = function(){}) {
    	// doSomething
    }
    

    触发默认值

    除了不传参数可以触发默认值外,当参数值是undefined时也可以触发默认值,但是null没有这个效果。

    function foo(url, timeout = 3000, callback = function(){}) {
    	console.log(timeout)
    }
    
    foo('/test') // 3000
    foo('/test', undefined) // 3000
    foo('/test', null) // null
    foo('/test', 5000) // 5000
    
    

    注意: 每次调用函数,默认参数值都会重新计算

    let n = 1;
    function foo(x = n + 1) {
      console.log(x)
    }
    
    foo() // 2
    n = 11
    foo() // 12
    

    length属性

    形参指定默认值后,函数的length属性返回没有指定默认值的参数个数。

    (function (a, b, c = 3) {}).length // 2
    

    rest 参数也不会计入length属性

    (function (a, b, ...args) {}).length // 2
    

    如果默认值的参数不是尾参数,那么length属性不再计入后面的参数

    (function (a, b = 3, c) {}).length // 1
    

    arguments

    ES6中,如果函数使用了参数默认值,arguments的对象行为同ES5严格模式下保持一致,即arguments对象中保存的是函数调用时传入的参数值。

    // ES5 非严格模式
    function foo(a) {
      console.log(arguments[0]) // 1
      a=2
      console.log(arguments[0]) // 2
    }
    foo(1)
    
    // ES5 严格模式
    function foo2(a) {
      'use strict'
      console.log(arguments[0]) // 1
      a=2
      console.log(arguments[0]) // 1
    }
    foo2(1)
    
    // ES6默认参
    function foo3(a = 1) {
      console.log(arguments[0]) // 1
      a=2
      console.log(arguments[0]) // 1
    }
    foo3(1)
    
    // ES6默认参
    function foo4(a = 1) {
      console.log(arguments.length) // 0
      a=2
      console.log(arguments[0]) // undefined
    }
    foo4()
    

    arguments.length等于传入参数的数量,所以foo4函数的arguments.length等于0,arguments[0]等于undefined

    默认参数表达式

    默认参数值可以是一个函数调用,参数值等于函数执行的返回值

    function test() {
      return 1
    }
    
    function foo(a, b=test()) {
      console.log(a + b)
    }
    
    foo(1,3) // 4
    foo(1) // 2
    

    注意: 当第一次调用foo函数时,由于传入了两个值,所以不会触发test函数的执行。

    临时死区

    function foo(a = b, b) {
      console.log(a + b)
    }
    
    foo(1,1) // 2
    foo(undefined, 1) // b is not defined
    

    在这个示例中,调用foo(undefined,1)函数,由于a初始化时b尚未初始化,所以会导致程序抛出错误,此时b尚处于临时死区中,所有引用临时死区中绑定的行为都会报错

    不定参数

    不定参数也称剩余参数或者rest参数,它的表示形式是在命名参数前加三个点...,这个参数在函数内部是一个数组,可以通过数组名访问里面的参数。

    function foo(a,b,...c) {
      console.log(c)
    }
    foo(1,2,3,4,5) // [3, 4, 5]
    
    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;
    }
    
    
    let person = {
      name: 'wmui',
      age: 10,
      sex: 'boy'
    }
    console.log(pick(person,'name','sex')) // {name: "wmui", sex: "boy"}
    

    使用限制

    1. 每个函数最多声明一个不定参数,并且必须放到所有参数的末尾。
    function foo(a,b,...c,d) {
      console.log(c)
    }
    foo(1,2,3,4,5) // Uncaught SyntaxError: Rest parameter must be last formal parameter
    
    1. 不定参数不能在对象字面量的setter属性中使用
    let o = {
      set name(...val) {
       // doSomething
      }
    }
    // Uncaught SyntaxError: Setter function argument must not be a rest parameter
    

    有这条限制也很好理解,因为本身对象字面量中setter的参数有且只能有一个,而不定参数的定义中,参数的数量可以无限多,所以在当前上下文中不允许使用不定参数

    arguments

    虽然有了不定参数,但是在ES6中arguments对象也是可以正常使用的,它并没有被不定参数取代。

    function foo(a,b,...c) {
      console.log(c.length) // 3
      console.log(arguments.length) // 5
    }
    foo(1,2,3,4,5)
    

    应用

    由于不定参数是一个数组,所以数组特有的方法都可以应用于该变量

    // arguments变量写法
    function sortNumbers() { 
      return Array.prototype.slice.call(arguments).sort()
    }
    
    // 不定参数写法
    let sortNumbers = (...args) => args.sort()
    

    展开运算符

    展开运算符和不定参数很相似。展开运算符可以把指定的数组,打散成各自独立的参数,然后传入函数;而不定参数是把各自独立的参数,整合成一个数组,然后在函数内部被访问。

    let arr = [1,2,3]
    console.log(...arr) // 1 2 3
    

    展开运算符通常用于需要传入多个独立参数的函数,比如用Math.max()方法获取一组数的最大值。

    Math.max()方法不能直接获取数组中的最大值,所有参数要以独立参数的形式传入

    Math.max(3,2,1) // 3
    
    // 利用apply()改变this,获取数组中元素最大值
    let arr = [1,2,3]
    Math.max.apply(Math, arr) // 3
    

    虽然可以借助apply()方法实现获取数组中元素最大值,但是第一眼很难看懂代码的真正意图,而利用展开运算符就要好很多。

    let arr = [1,2,3]
    Math.max(...arr) // 3
    

    展开运算符还可以和正常传入的参数混合使用,比如设置Math.max()返回值最小为0

    let arr = [-1,-2,-3]
    Math.max(...arr, 0) // 0
    

    展开运算符可以简化使用数组给函数传参的编码过程,在大多数需要使用apply()方法的情况下展开运算符可能是一个更合适的方案

    严格模式

    从ES5开始,函数内部可以设置为严格模式。ES7对严格模式做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错

    之所以这样规定,是因为函数内部的严格模式同时适用于函数参数和函数体,但函数执行的顺序是先执行函数参数再执行函数体,这样就会导致可能你的函数参数是不符合严格模式的,但要到执行函数体时才能被检测到,这无疑是不合理的。

    function doSomething(value = 070) {
      'use strict';
      return value;
    }
    

    严格模式下是不允许用前缀0代表八进制的,如果ES7不修改严格模式,那么JS引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错

    参数尾逗号

    ES8允许函数的最后一个参数有尾逗号(trailing comma)。

    function fn(
      param1,
      param2,
    ) { /* ... */ }
    
    fn(
      'foo',
      'bar',
    );
    

    这样的规定使得函数参数与数组和对象的尾逗号规则保持一致

    name属性

    JS中有多种定义函数的方式,因而辨别函数就是一项具有挑战性的任务,ES6为所有函数新增了name属性,方便开发者们追踪函数调用记录

    // 示例1  函数名字是声明时函数的名称
    function foo(){}
    console.log(foo.name) // foo
    
    // 示例2  函数名字是匿名函数变量的名称
    let foo2 = function(){}
    console.log(foo2.name) // foo2
    
    // 示例3  函数名字是函数表达式自身的名称
    let foo3 = function test(){}
    console.log(foo3.name) // test
    
    // 示例4  函数名字带有bound前缀
    let foo4 = function (){}
    console.log(foo4.bind().name) // bound foo4
    
    // 示例5  函数名字带有anonymous前缀
    let foo5 = new Function()
    console.log(foo5.name) // anonymous
    
    // 示例6
    let obj = {
      get getName() {
       return 'wmui'
      },
      set setName(v) {
       this.name = v
      },
      sayName() {
       return this.name
      }
    }
    
    let descriptor = Object.getOwnPropertyDescriptor(obj, 'getName');
    let descriptor2 = Object.getOwnPropertyDescriptor(obj, 'setName');
    // getter函数带有get前缀,setter函数带有set前缀
    console.log(obj.sayName.name) // sayName
    console.log(descriptor.get.name) // get getName
    console.log(descriptor2.set.name) // set getName
    

    判断调用

    JS函数内部有两个内部方法:[[call]]和[[construct]]

    当通过new关键字调用函数时,执行的是[[construct]]函数,它会创建一个新的实例对象,函数体执行时会把this绑定到实例上。

    如果不使用new关键字调用函数,则执行[[call]]函数,直接执行代码中的函数体

    注意: 不是所有函数都有[[construct]]方法,所以不是所有函数都可以通过new来调用。具有[[construct]]方法的函数被统称为构造函数

    ES5判断函数调用

    在ES5中判断一个函数是否通过new关键字调用,最常用的方法是使用instanceof操作符

    function Person(name) {
      if(this instanceof Person) {
        this.name = name
      } else {
        throw new Error('You must use new with Person')
      }
    }
    
    let p1 = new Person('wmui')
    let p2 = Person('wmui') // 报错
    

    这种做法是正确的,但是并不完全靠得住,因为当使用call()或apply()方法强制把this绑定到Person实例上时,它是检测不会出来的。

    function Person(name) {
      if(this instanceof Person) {
        this.name = name
      } else {
        throw new Error('You must use new with Person')
      }
    }
    
    let instance = new Person()
    let p3 = Person.call(instance, 'wmui') // 不报错
    

    ES6判断函数调用

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

    function Person(name) {
      if(typeof new.target !== "undefined") {
        this.name = name
      } else {
        throw new Error('You must use new with Person')
      }
    }
    
    let p1 = new Person('wmui')
    let p2 = Person('wmui') // 报错
    
    let instance = new Person()
    let p3 = Person.call(instance, 'wmui') // 报错
    

    块级函数

    在代码块中声明的函数就是块级函数。在ES6之前定义块级函数严格来说是一个语法错误,虽然浏览器也支持,但是表现行为不完全一致。而ES6会把函数视为一个块级声明,从而可以在代码块中声明和访问该函数。

    if(true) {
      function foo() {
        console.log('hello')
      }
      foo()
    }
    
    foo()
    // 'hello'
    // 'hello'
    

    非严格模式下,代码块内定义的函数,在代码块外仍然可以访问到,这是因为函数声明被提升到了外围函数或全局作用域的顶部

    严格模式下,代码块内定义的函数,在代码块外访问不到,这是因为if语句代码块结束执行后,语句内的函数也不存在了。

    'use strict';
    if(true) {
      function foo() {
       console.log('hello')
      }
      foo()
    }
    
    foo()
    // 'hello'
    // Uncaught ReferenceError: foo is not defined
    

    箭头函数

    箭头函数(=>)是一种使用箭头定义函数的新语法,它与传统的JS函数有一些不同:

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

    2. 不能通过new关键字调用
      因为箭头函数没有[[construct]]方法

    3. 没有原型
      由于不能通过new关键字调用箭头函数,因而没有构建原型的需求,所以没有prototype属性

    4. 不能改变this绑定
      箭头函数内部的this值不可以被改变,在函数声明周期内始终保持一致

    5. 不支持arguments对象
      箭头函数没有arguments对象,必须通过命名参数和不定参数这两种形式来访问其参数

    6. 不支持重复的命名参数
      无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数;而传统函数只有在严格模式下,才不能有重复的命名参数

    语法

    箭头函数有多种不同的表现形式,但都有参数、箭头和函数体组成。

    let foo = num => num + 1;
    
    // 有效等价于
    let foo = function(num) {
      return num + 1;
    }
    

    如果只有一个参数,可以直接写参数名,然后是箭头,箭头右侧的表达式被求值后会立即返回。

    如果有两个或两个以上参数,要在参数两侧加上一对小括号。

    let foo = (num1, num2) => num1 + num2;
    
    // 有效等价于
    let foo = function(num1, num2) {
      return num1 + num2;
    }
    

    如果函数没有参数,要在声明的时候写一组没有内容的小括号

    let foo = () => 1;
    
    // 有效等价于
    let foo = function() {
      return 1;
    }
    

    如果希望为函数编写由多个表达式组成的更传统的函数体,那么需要用花括号包裹函数体,并显式地定义一个返回值

    let foo = (num1, num2) => {
      return num1 + num2;
    }
    
    // 有效等价于
    let foo = function(num1, num2) {
      return num1 + num2;
    }
    

    this

    箭头函数中没有this绑定,必须通过查找作用城链来决定其值。

    如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined

    如果对象的方法中包含了另外一个函数,并且这个函数引用了this,为了不让this指向window对象,我们通常会使用bind()显示的为函数绑定this值

    let obj = {
      name: 'wmui',
      init: function() {
       document.addEventListener('click', (function(e){
         // 方法内部的函数引用了this,使用bind()改变this指向
         this.test(e.type)
       }).bind(this), false)
      },
      test: function(type) {
       console.log(this.name,type)
      }
    }
    
    obj.init() // wmui click
    

    如果用箭头函数重写上面的示例,不仅使代码更精简,而且更加容易理解

    let obj = {
      name: 'wmui',
      init: function() {
       document.addEventListener('click', (e) => {
         this.test(e.type)
       }, false)
      },
      test: function(type) {
       console.log(this.name,type)
      }
    }
    
    obj.init() // wmui click
    

    辨识方法

    尽管箭头函数与传统函数的语法不同,但它同样可以被识别出来

    let foo = (num1, num2) => num1 - num2;
    console.log(typeof foo); // "function"
    console.log(foo instanceof Function); // true
    

    箭头函数上可以调用call()、apply()及bind()方法,但箭头函数的this值不会受这些方法的影响

    let foo = (num1, num2) => num1 + num2;
    console.log(foo.call(null, 1, 2)); // 3
    console.log(foo.apply(null, [1, 2])); // 3
    
    let boundFoo = foo.bind(null, 1, 2);
    console.log(boundFoo()); // 3
    

    函数柯里化

    柯里化是一种可以把多参函数转变成单参函数,并且调用后返回一个新函数的技术,这个新函数可以接收剩余参数而且有返回结果

    使用ES5的语法写一个柯里化函数

    function foo(x) {
      return function (y) {
       return y + x
      }
    }
    foo(1)(2) // 3
    

    使用ES6的语法写一个柯里化函数

    let foo (x) => (y) => y + x;
    
    foo(1)(2) // 3
    

    一般来说,出现连续地箭头函数调用的情况,就是在使用函数柯里化的技术

    尾调用优化

    尾调用是指函数作为另一个函数的最后一条语句被调用。

    关于函数的调用这里简单说一下,函数调用会在内存中形成一个调用记录,称作“调用帧”(call frame),用来保存调用位置和内部变量等信息。如果在函数A的内部调用了函数B,那么在A的调用帧上方就会形成一个B的调用帧,等到函数B运行结束并且将结果返回到A,B的调用帧才会消失。同理,如果函数B的内部调用了函数C,那么在B的调用帧上方会有一个C的调用帧,以此类推,所有的调用帧就会形成一个调用栈(call stack)

    尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,于是JS引擎就可以在背后对尾调用进行优化。如果所有函数都是尾调用,将会大大节省内存开销。

    ES6缩减了严格模式下尾调用栈的大小,如果满足以下三个条件,尾调用不再创建新的栈帧,并且可以被JS引擎自动优化:

    1. 尾调用不是闭包
    2. 尾调用是函数内部的最后一条语句
    3. 尾调用的结果作为返回值被返回
    'use strict';
    function foo() {
      // 被优化
      return foo2()
    }
    

    下面这几种情况不会被优化:

    // 示例1  缺少return语句
    'use strict';
    function foo() {
      foo2()
    }
    
    // 示例2  尾调用返回后执行其他操作
    'use strict';
    function foo() {
      return foo2() + 1
    }
    
    // 示例3  不是尾调用
    'use strict';
    function foo() {
      let t = foo2()
      return t
    }
    
    // 示例4  尾调用是闭包
    'use strict';
    function foo() {
      let num = 1
      let t = () => num
      return t()
    }
    

    应用

    尾调用优化常被用于递归函数

    应用一:计算阶乘

    function factorial(n) {
      if (n <= 1) {
          return 1;
        } else {
          // 未被优化
          return n * factorial(n - 1);
      }
    }
    
    function factorial(n, p = 1) {
      if (n <= 1) {
        return 1 * p;
      } else {
        let result = n * p;
        // 被优化
        return factorial(n - 1, result);
      }
    }
    

    应用二:计算Fibonacci数列

    function Fibonacci (n) {
      if ( n <= 1 ) {return 1};
      return Fibonacci(n - 1) + Fibonacci(n - 2);
    }
    
    Fibonacci(10) // 89
    Fibonacci(100) // 堆栈溢出
    
    function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
      if( n <= 1 ) {return ac2};
      return Fibonacci2 (n - 1, ac2, ac1 + ac2);
    }
    
    Fibonacci2(100) // 573147844013817200000
    

    ES6 明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存

    优秀文章首发于聚享小站,欢迎关注!
  • 相关阅读:
    你知道线框图和原型有啥区别吗
    Git 创建与合并分支
    Java 多态 接口继承等学习笔记
    JAVA多线程之生产者 消费者模式 妈妈做面包案例
    Robot Framework自动化测试(一)---第一个脚本
    仲夏之夜
    vulstack红队评估(一)
    CVE-2017-7269-iis远程溢出漏洞复现
    JMeter 控件整理
    JMeter函数整理
  • 原文地址:https://www.cnblogs.com/yesyes/p/15352138.html
Copyright © 2011-2022 走看看