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 中只要使用尾递归,就不会发生栈溢出,相对节省内存

    优秀文章首发于聚享小站,欢迎关注!
  • 相关阅读:
    HDU 4069 Squiggly Sudoku
    SPOJ 1771 Yet Another NQueen Problem
    POJ 3469 Dual Core CPU
    CF 118E Bertown roads
    URAL 1664 Pipeline Transportation
    POJ 3076 Sudoku
    UVA 10330 Power Transmission
    HDU 1426 Sudoku Killer
    POJ 3074 Sudoku
    HDU 3315 My Brute
  • 原文地址:https://www.cnblogs.com/yesyes/p/15352138.html
Copyright © 2011-2022 走看看