ES6学习4(函数的拓展)
来源:互联网 发布:福州专业淘宝美工培训 编辑:程序博客网 时间:2024/05/19 06:38
函数参数的默认值
基本用法
在ES6之前,不能直接为函数的参数指定默认值,于是只能使用变通的方法。在函数内部手动检测参数是否有值,不过这样的方法有时并不保险。
ES6允许直接写上默认值。
function log(x, y = 'World') { console.log(x, y);}log('Hello') // Hello Worldlog('Hello', 'China') // Hello Chinalog('Hello', '') // Hello
这样写不仅简化了写法,更加可靠,而且还有两个好处,只要一看函数的参数表,马上就知道哪些参数是必须的,哪些不用传也行,不传时默认值是什么。将来包装这个函数或写对外接口时,可以直接剪掉这个参数,也不会有什么问题。
与解构赋值默认值结合使用
如果只使用解构赋值的默认值,那就意味着还是必须要传一个参数才行。
function foo({x, y = 5}) { console.log(x, y);}foo({}) // undefined, 5foo({x: 1}) // 1, 5foo({x: 1, y: 2}) // 1, 2foo() // TypeError: Cannot read property 'x' of undefined
这里如果传入的参数不是一个对象,那么解构赋值就不会执行,x,y也就不会生成。如果同时使用函数参数的默认值,就可以不传参数了。
function foo({x, y = 5}={}) { console.log(x, y);}foo({}) // undefined, 5foo({x: 1}) // 1, 5foo({x: 1, y: 2}) // 1, 2foo() // undefined, 5
参数默认值的位置
通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
function f(x, y = 5, z) { return [x, y, z];}f() // [undefined, 5, undefined]f(1) // [1, 5, undefined]f(1, ,2) // 报错f(1, undefined, 2) // [1, 5, 2]
函数的length属性
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。
(function (a) {}).length // 1(function (a = 5) {}).length // 0(function (a, b, c = 5) {}).length // 2
这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length属性。
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0(function (a, b = 1, c) {}).length // 1
作用域
如果参数默认值是一个变量,则该变量所处的作用域,与其他变量的作用域规则是一样的,即先是当前函数的作用域,然后才是全局作用域。
内部的x已经生成:
var x = 1;function f(x, y = x) { console.log(y);}f(2) // 2
内部的x并未生成:
let x = 1;function f(y = x) { let x = 2; console.log(y);}f() // 1
如果给y赋值时x并不存在则会报错:
function f(y = x) { let x = 2; console.log(y);}f() // ReferenceError: x is not defined
由于函数的参数其实也是隐式的使用let声明的,所以下面这样写会触发暂时性死区,是读不到外面作用域中的x的:
var x4 = 1;function f4(x4 = x4) { console.log(x4);}f4(); // ReferenceError: x is not defined
如果参数的默认值是一个函数,该函数的作用域是其声明时所在的作用域:
let foo = 'outer';function bar(func = x => foo) { let foo = 'inner'; console.log(func()); // outer}bar();
上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。这个匿名函数声明时,bar函数的作用域还没有形成,所以匿名函数里面的foo指向外层作用域的foo,输出outer。
这里使用babel转码的时候会转为这样:
var foo = 'outer';function bar() { var func = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function (x) { return foo; }; var foo = 'inner'; console.log(func()); }bar();
这时输出的就是inner了,因为func是在函数里定义的。不知道这个问题会怎么解决。
应用
利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() { throw new Error('Missing parameter');}function foo1(mustBeProvided = throwIfMissing()) { return mustBeProvided;}foo1();
rest参数
ES6引入rest参数(形式为“…变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum;}console.log(add(2, 5, 3)); // 10
与arguments不同,这里的value真的是一个数组,所以数组特有的方法都可以用于这个变量。注意,rest参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。函数的length属性,不包括rest参数。
拓展运算符
扩展运算符(spread)是三个点(…)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。该运算符主要用于函数调用。
function push(array, ...items) { array.push(...items);}
扩展运算符与正常的函数参数可以结合使用。
function f(v, w, x, y, z) { console.log(v+","+w+","+x+","+y+","+z);}var args = [0, 1];f(-1, ...args, 2, ...[3]);
应用
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。
// ES5的写法Math.max.apply(null, [14, 3, 77])// ES6的写法Math.max(...[14, 3, 77])
还有一些其他的应用:
var arr1 = ['a', 'b'];var arr2 = ['c'];var arr3 = ['d', 'e'];// ES6的合并数组[...arr1, ...arr2, ...arr3];//与解构赋值结合使用const [first, ...rest] = [1, 2, 3, 4, 5];first // 1rest // [2, 3, 4, 5]const [first, ...rest] = [];first // undefinedrest // []:const [first, ...rest] = ["foo"];first // "foo"rest // []
对于字符串,拓展运算符可以将其转化为真正的数组,并且可以正确的识别32位的Unicode字符。因此,正确返回字符串长度的函数,可以像下面这样写。
function length(str) { return [...str].length;}length('x\uD83D\uDE80y') // 3
凡是涉及到操作32位Unicode字符的函数,都有这个问题。因此,最好都用扩展运算符改写。
let str = 'x\uD83D\uDE80y';str.split('').reverse().join('')// 'y\uDE80\uD83Dx'[...str].reverse().join('')// 'y\uD83D\uDE80x'
任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。
var nodeList = document.querySelectorAll('div');var array = [...nodeList];
name属性
var func1 = function () {};// ES5func1.name // ""// ES6func1.name // "func1"
const bar = function baz() {};// ES5bar.name // "baz"// ES6bar.name // "baz"
比较特殊的是bind返回的函数:
function foo() {};foo.bind({}).name // "bound foo"(function(){}).bind({}).name // "bound "
箭头函数
基本用法:
var f = v => v;//等同于var f = function(v) { return v;};var f = () => 5;// 等同于var f = function () { return 5 };var sum = (num1, num2) => num1 + num2;// 等同于var sum = function(num1, num2) { return num1 + num2;};
由于{}代表着代码块,要直接返回对象的函数要使用({})的形式。
var getTempItem = id => ({ id: id, name: "Temp" });
与变量解构同时使用:
const full = ({ first, last }) => first + ' ' + last;// 等同于function full(person) { return person.first + ' ' + person.last;}
利用这个特性可以简化回调函数:
// 正常函数写法[1,2,3].map(function (x) { return x * x;});// 箭头函数写法[1,2,3].map(x => x * x);
几个比较巧妙的用法:
const numbers = (...nums) => nums;numbers(1, 2, 3, 4, 5)// [1,2,3,4,5]const headAndTail = (head, ...tail) => [head, tail];headAndTail(1, 2, 3, 4, 5)// [1,[2,3,4,5]]
注意事项
函数体内的this对象就是定义时所在的对象,而不是使用时所在的对象。
一般来说,this对象的指向是可变的,比如使用call或函数实际执行时的环境发生了变化。但是箭头函数并不是这样的,它的this对象在定义函数时就已经确定了,不会发生改变。
var s2 = 0;function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000);}var timer = new Timer();setTimeout(() => console.log('s1: ', timer.s1), 3100);setTimeout(() => console.log('s2in: ', timer.s2), 3100);setTimeout(() => console.log('s2out: ', s2), 3100);// s1: 3// s2in: 0// s2out: 3
这里setInterval执行时是处于全局作用域的,普通函数的this这时是全局变量,可以看到timer的s2一直没有被更新,因为执行时this是全局变量,但是timer的s1是一直被更新的,因为这个函数中的this一开始就被绑定在了timer。
箭头函数中this的固定并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。
如果我们看一下babel对箭头函数的处理就会更加清楚:
// ES6function foo() { setTimeout(() => { console.log('id:', this.id); }, 100);}// ES5function foo() { var _this = this; setTimeout(function () { console.log('id:', _this.id); }, 100);}
箭头函数里根本就没有this。
所以当箭头函数嵌套的时候,所有的this都是最外层箭头函数所在作用域中的this:
function foo() { return () => { return () => { return () => { console.log('id:', this.id); }; }; };}var f = foo.call({id: 1});var t1 = f.call({id: 2})()(); // id: 1var t2 = f().call({id: 3})(); // id: 1var t3 = f()().call({id: 4}); // id: 1
由于没有自己的this,箭头函数自然也就不能用做构造函数,也就不能用call()、apply()、bind()这些方法去改变this的指向。
arguments、super、new.target在箭头函数中也是不存在的,指向外层函数的对应变量
除此之外,箭头函数还不可以使用yield命令,因此箭头函数不能用作Generator函数。
嵌套箭头函数
function insert(value) { return {into: function (array) { return {after: function (afterValue) { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; }}; }};}insert(2).into([1, 3]).after(1); //[1, 2, 3]
使用箭头函数改写以后:
let insert = (value) => ({into: (array) => ({after: (afterValue) => { array.splice(array.indexOf(afterValue) + 1, 0, value); return array;}})});insert(2).into([1, 3]).after(1); //[1, 2, 3]
const pipeline = (...funcs) => val => funcs.reduce((a, b) => b(a), val);const plus1 = a => a + 1;const mult2 = a => a * 2;const addThenMult = pipeline(plus1, mult2);addThenMult(5)
函数绑定
函数绑定运算符是并排的两个双冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。
foo::bar;// 等同于bar.bind(foo);foo::bar(...arguments);// 等同于bar.apply(foo, arguments);var method = obj::obj.foo;// 等同于var method = ::obj.foo;
尾调用优化
尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。
这种情况就属于尾调用:
function f(x){ return g(x);}function f(x) { if (x > 0) { return m(x) } return n(x);}
以下3种情况都不是尾调用:
// 情况一function f(x){ let y = g(x); return y;}// 情况二function f(x){ return g(x) + 1;}// 情况三function f(x){ g(x);}
函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。
尾递归
递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
比如说著名的fibonacci数列计算,如果使用非尾递归:
function Fibonacci (n) { if ( n <= 1 ) {return 1}; return Fibonacci(n - 1) + Fibonacci(n - 2);}Fibonacci(10); // 89// Fibonacci(100)// Fibonacci(500)// 堆栈溢出了
换一种想法,从头开始计算,使用尾递归:
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) { if( n <= 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 + ac2);}Fibonacci2(100) // 573147844013817200000Fibonacci2(1000) // 7.0330367711422765e+208Fibonacci2(10000) // Infinity
这样调用栈就不会溢出了。
严格模式
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
尾递归优化的实现
对于不支持尾调用优化的环境,我们也可以自己实现尾调用优化,主要的思想就是将嵌套的调用转换为循环
function tco(f) { var value; var active = false; var accumulated = []; return function accumulator() { accumulated.push(arguments); if (!active) { active = true; while (accumulated.length) { value = f.apply(this, accumulated.shift()); } active = false; return value; } };}var sum = tco(function(x, y) { if (y > 0) { return sum(x + 1, y - 1) } else { return x }});sum(1, 100000)// 100001
- ES6学习4(函数的拓展)
- ES6 函数的拓展
- ES6基础教程(5)-函数的拓展
- ES6学习5(对象的拓展)
- ES6学习3(各种类型的拓展)
- ES6之函数的拓展(部分)
- ES6学习笔记(字符串拓展)
- ES6基础教程(4)-字符串拓展
- ES6学习之路(四) 数组拓展
- es6学习之路(7):函数的扩展
- es6---解构赋值与字符串的拓展
- ES6之字符串的拓展(部分)
- ES6之数组的拓展(部分)
- ES6 学习笔记之《函数的扩展》
- 9、字符串、数组、对象等内置对象的拓展—ES6学习笔记
- es6 学习笔记(一)箭头函数
- 函数的参数拓展
- ES6学习笔记(ES6新增的数组方法)
- Spring Boot使用方法小札(1):Web应用返回jsp页面
- 写MD5加密
- 利用栈将中缀表达式转换为后缀表达式
- C初级阶段练习题目(三)
- MacOS删除vpn
- ES6学习4(函数的拓展)
- arpspoof毒化
- 类模板练习题——Template Arithmetic
- 程序员提高效率的必备工具
- (三)JavaScript 的运算符,条件语句,循环语句..
- 电商之梳理mina相关知识---框架
- 电商之梳理jetty相关知识---服务器
- KMP算法-Java实现
- Node.js简单操作MongoDB(CRUD)