当前位置: 首页 > news >正文

内蒙古建信建设有限公司网站wordpress 数据库发布

内蒙古建信建设有限公司网站,wordpress 数据库发布,最流行的网站设计风格,企业网站推广多少钱目录 1. 函数参数的默认值 1.1基本用法 1.2与解构赋值默认值结合使用 1.3参数默认值的位置 1.4函数的 length 属性 1.5作用域 1.6应用 2. rest 参数 3. 严格模式 4. name 属性 5. 箭头函数 5.1基本用法 5.2使用注意点 5.3嵌套的箭头函数 6. 双冒号运算符 7. 尾…目录 1. 函数参数的默认值 1.1基本用法 1.2与解构赋值默认值结合使用 1.3参数默认值的位置 1.4函数的 length 属性  1.5作用域 1.6应用 2. rest 参数 3. 严格模式 4. name 属性 5. 箭头函数 5.1基本用法 5.2使用注意点 5.3嵌套的箭头函数 6. 双冒号运算符 7. 尾调用优化 7.1什么是尾调用 7.2尾调用优化 7.3尾递归 7.4递归函数改写 7.5严格模式 7.6尾递归优化的实现 8. 函数参数的尾逗号 9. catch 语句的参数 总结 “睡服“面试官系列之各系列目录汇总建议学习收藏 1. 函数参数的默认值 1.1基本用法 ES6 之前不能直接为函数的参数指定默认值只能采用变通的方法。 function log(x, y) { y y || World; console.log(x, y); } log(Hello) // Hello World log(Hello, China) // Hello China log(Hello, ) // Hello World 上面代码检查函数 log 的参数 y 有没有赋值如果没有则指定默认值为 World 。这种写法的缺点在于如果参数 y 赋值了但是对应的布尔值为 false 则该赋值不起作用。就像上面代码的最后一行参数 y 等于空字符结果被改为默认值。 为了避免这个问题通常需要先判断一下参数 y 是否被赋值如果没有再等于默认值 if (typeof y undefined) { y World; } ES6 允许为函数的参数设置默认值即直接写在参数定义的后面 function log(x, y World) { console.log(x, y); } log(Hello) // Hello World log(Hello, China) // Hello China log(Hello, ) // Hello 可以看到ES6 的写法比 ES5 简洁许多而且非常自然。下面是另一个例子 function Point(x 0, y 0) { this.x x; this.y y; } const p new Point(); p // { x: 0, y: 0 } 除了简洁ES6 的写法还有两个好处首先阅读代码的人可以立刻意识到哪些参数是可以省略的不用查看函数体或文档其次有利于将来的代码 优化即使未来的版本在对外接口中彻底拿掉这个参数也不会导致以前的代码无法运行。 参数变量是默认声明的所以不能用 let 或 const 再次声明。 function foo(x 5) { let x 1; // error const x 2; // error } 上面代码中参数变量 x 是默认声明的在函数体中不能用 let 或 const 再次声明否则会报错。 使用参数默认值时函数不能有同名参数 // 不报错 function foo(x, x, y) { // ... } // 报错 function foo(x, x, y 1) { // ... } // SyntaxError: Duplicate parameter name not allowed in this context 另外一个容易忽略的地方是参数默认值不是传值的而是每次都重新计算默认值表达式的值。也就是说参数默认值是惰性求值的 let x 99; function foo(p x 1) { console.log(p); } foo() // 100 x 100; foo() // 101 上面代码中参数 p 的默认值是 x 1 。这时每次调用函数 foo 都会重新计算 x 1 而不是默认 p 等于 100。 1.2与解构赋值默认值结合使用 参数默认值可以与解构赋值的默认值结合起来使用。 function foo({x, y 5}) { console.log(x, y); } foo({}) // undefined 5 foo({x: 1}) // 1 5 foo({x: 1, y: 2}) // 1 2 foo() // TypeError: Cannot read property x of undefined 上面代码只使用了对象的解构赋值默认值没有使用函数参数的默认值。只有当函数 foo 的参数是一个对象时变量 x 和 y 才会通过解构赋值生成。如果 函数 foo 调用时没提供参数变量 x 和 y 就不会生成从而报错。通过提供函数参数的默认值就可以避免这种情况 function foo({x, y 5} {}) { console.log(x, y); } foo() // undefined 5 上面代码指定如果没有提供参数函数 foo 的参数默认为一个空对象。 下面是另一个解构赋值默认值的例子 function fetch(url, { body , method GET, headers {} }) { console.log(method); } fetch(http://example.com, {}) // GET fetch(http://example.com) // 报错 上面代码中如果函数 fetch 的第二个参数是一个对象就可以为它的三个属性设置默认值。这种写法不能省略第二个参数如果结合函数参数的默认 值就可以省略第二个参数。这时就出现了双重默认值。 function fetch(url, { body , method GET, headers {} } {}) { console.log(method); } fetch(http://example.com) // GET 上面代码中函数 fetch 没有第二个参数时函数参数的默认值就会生效然后才是解构赋值的默认值生效变量 method 才会取到默认值 GET 。 作为练习请问下面两种写法有什么差别 // 写法一 function m1({x 0, y 0} {}) { return [x, y]; } // 写法二 function m2({x, y} { x: 0, y: 0 }) { return [x, y]; } 上面两种写法都对函数的参数设定了默认值区别是写法一函数参数的默认值是空对象但是设置了对象解构赋值的默认值写法二函数参数的默认值是 一个有具体属性的对象但是没有设置对象解构赋值的默认值。 // 函数没有参数的情况 m1() // [0, 0] m2() // [0, 0] // x 和 y 都有值的情况 m1({x: 3, y: 8}) // [3, 8] m2({x: 3, y: 8}) // [3, 8] // x 有值y 无值的情况 m1({x: 3}) // [3, 0] m2({x: 3}) // [3, undefined] // x 和 y 都无值的情况 m1({}) // [0, 0]; m2({}) // [undefined, undefined] m1({z: 3}) // [0, 0] m2({z: 3}) // [undefined, undefined] 1.3参数默认值的位置 通常情况下定义了默认值的参数应该是函数的尾参数。因为这样比较容易看出来到底省略了哪些参数。如果非尾部的参数设置默认值实际上这个 参数是没法省略的。 // 例一 function f(x 1, y) { return [x, y]; } f() // [1, undefined] f(2) // [2, undefined]) f(, 1) // 报错 f(undefined, 1) // [1, 1] // 例二 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] 上面代码中有默认值的参数都不是尾参数。这时无法只省略该参数而不省略它后面的参数除非显式输入 undefined 。 如果传入 undefined 将触发该参数等于默认值 null 则没有这个效果。 function foo(x 5, y 6) { console.log(x, y); } foo(undefined, null) // 5 null 上面代码中 x 参数对应 undefined 结果触发了默认值 y 参数等于 null 就没有触发默认值。 1.4函数的 length 属性  指定了默认值以后函数的 length 属性将返回没有指定默认值的参数个数。也就是说指定了默认值后 length 属性将失真 (function (a) {}).length // 1 (function (a 5) {}).length // 0 (function (a, b, c 5) {}).length // 2 上面代码中 length 属性的返回值等于函数的参数个数减去指定了默认值的参数个数。比如上面最后一个函数定义了 3 个参数其中有一个参数 c 指定了默认值因此 length 属性等于 3 减去 1 最后得到 2 。 这是因为 length 属性的含义是该函数预期传入的参数个数。某个参数指定默认值以后预期传入的参数个数就不包括这个参数了。同理后文的 rest 参数也不会计入 length 属性 (function(...args) {}).length // 0 如果设置了默认值的参数不是尾参数那么 length 属性也不再计入后面的参数了 (function (a 0, b, c) {}).length // 0 (function (a, b 1, c) {}).length // 1 1.5作用域 一旦设置了参数的默认值函数进行声明初始化时参数会形成一个单独的作用域context。等到初始化结束这个作用域就会消失。这种语法行为 在不设置参数默认值时是不会出现的。 var x 1; function f(x, y x) { console.log(y); } f(2) // 2 上面代码中参数 y 的默认值等于变量 x 。调用函数 f 时参数形成一个单独的作用域。在这个作用域里面默认值变量 x 指向第一个参数 x 而不是全局 变量 x 所以输出是 2 。 再看下面的例子 let x 1; function f(y x) { let x 2; console.log(y); } f() // 1 上面代码中函数 f 调用时参数 y x 形成一个单独的作用域。这个作用域里面变量 x 本身没有定义所以指向外层的全局变量 x 。函数调用时函 数体内部的局部变量 x 影响不到默认值变量 x 。 如果此时全局变量 x 不存在就会报错 function f(y x) { let x 2; console.log(y); } f() // ReferenceError: x is not defined 下面这样写也会报错。 var x 1; function foo(x x) { // ... } foo() // ReferenceError: x is not defined 上面代码中参数 x x 形成一个单独作用域。实际执行的是 let x x 由于暂时性死区的原因这行代码会报错”x 未定义“。 如果参数的默认值是一个函数该函数的作用域也遵守这个规则。请看下面的例子 let foo outer; function bar(func () foo) { let foo inner; console.log(func()); } bar(); // outer 上面代码中函数 bar 的参数 func 的默认值是一个匿名函数返回值为变量 foo 。函数参数形成的单独作用域里面并没有定义变量 foo 所以 foo 指向 外层的全局变量 foo 因此输出 outer 。 如果写成下面这样就会报错 function bar(func () foo) { let foo inner; console.log(func()); } bar() // ReferenceError: foo is not defined 上面代码中匿名函数里面的 foo 指向函数外层但是函数外层并没有声明变量 foo 所以就报错了。 下面是一个更复杂的例子 var x 1; function foo(x, y function() { x 2; }) { var x 3; y(); console.log(x); } foo() // 3 x // 1 上面代码中函数 foo 的参数形成一个单独作用域。这个作用域里面首先声明了变量 x 然后声明了变量 y y 的默认值是一个匿名函数。这个匿名函 数内部的变量 x 指向同一个作用域的第一个参数 x 。函数 foo 内部又声明了一个内部变量 x 该变量与第一个参数 x 由于不是同一个作用域所以不是同 一个变量因此执行 y 后内部变量 x 和外部全局变量 x 的值都没变。 如果将 var x 3 的 var 去除函数 foo 的内部变量 x 就指向第一个参数 x 与匿名函数内部的 x 是一致的所以最后输出的就是 2 而外层的全局变量 x 依然不受影响。 var x 1; function foo(x, y function() { x 2; }) { x 3; y(); console.log(x); } foo() // 2 x // 1 1.6应用 利用参数默认值可以指定某一个参数不得省略如果省略就抛出一个错误。 function throwIfMissing() { throw new Error(Missing parameter); } function foo(mustBeProvided throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter 上面代码的 foo 函数如果调用的时候没有参数就会调用默认值 throwIfMissing 函数从而抛出一个错误。 从上面代码还可以看到参数 mustBeProvided 的默认值等于 throwIfMissing 函数的运行结果注意函数名 throwIfMissing 之后有一对圆括号这表 明参数的默认值不是在定义时执行而是在运行时执行。如果参数已经赋值默认值中的函数就不会运行。 另外可以将参数默认值设为 undefined 表明这个参数是可以省略的。 function foo(optional undefined) { ··· } 2. rest 参数 ES6 引入 rest 参数形式为 ...变量名 用于获取函数的多余参数这样就不需要使用 arguments 对象了。rest 参数搭配的变量是一个数组该变量 将多余的参数放入数组中。 function add(...values) { let sum 0; for (var val of values) { sum val; } return sum; } add(2, 5, 3) // 10 上面代码的 add 函数是一个求和函数利用 rest 参数可以向该函数传入任意数目的参数。 下面是一个 rest 参数代替 arguments 变量的例子。 // arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // rest参数的写法 const sortNumbers (...numbers) numbers.sort(); 上面代码的两种写法比较后可以发现rest 参数的写法更自然也更简洁。 arguments 对象不是数组而是一个类似数组的对象。所以为了使用数组的方法必须使用 Array.prototype.slice.call 先将其转为数组。rest 参数就 不存在这个问题它就是一个真正的数组数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组 push 方法的例子。 function push(array, ...items) { items.forEach(function(item) { array.push(item); console.log(item); }); } var a []; push(a, 1, 2, 3) 注意rest 参数之后不能再有其他参数即只能是最后一个参数否则会报错。 // 报错 function f(a, ...b, c) { // ... } 函数的 length 属性不包括 rest 参数。 (function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 1 3. 严格模式 从 ES5 开始函数内部可以设定为严格模式 function doSomething(a, b) { use strict; // code } ES2016 做了一点修改规定只要函数参数使用了默认值、解构赋值、或者扩展运算符那么函数内部就不能显式设定为严格模式否则会报错。 // 报错 function doSomething(a, b a) { use strict; // code } // 报错 const doSomething function ({a, b}) { use strict; // code }; // 报错 const doSomething (...a) { use strict; // code }; const obj { // 报错 doSomething({a, b}) { use strict; // code 这样规定的原因是函数内部的严格模式同时适用于函数体和函数参数。但是函数执行的时候先执行函数参数然后再执行函数体。这样就有一个 不合理的地方只有从函数体之中才能知道参数是否应该以严格模式执行但是参数却应该先于函数体执行 // 报错 function doSomething(value 070) { use strict; return value; } 上面代码中参数 value 的默认值是八进制数 070 但是严格模式下不能用前缀 0 表示八进制所以应该报错。但是实际上JavaScript 引擎会先成功执 行 value 070 然后进入函数体内部发现需要用严格模式执行这时才会报错。 虽然可以先解析函数体代码再执行参数代码但是这样无疑就增加了复杂性。因此标准索性禁止了这种用法只要参数使用了默认值、解构赋值、或 者扩展运算符就不能显式指定严格模式。 两种方法可以规避这种限制。第一种是设定全局性的严格模式这是合法的。 use strict; function doSomething(a, b a) { // code } 第二种是把函数包在一个无参数的立即执行函数里面。 const doSomething (function () { use strict; return function(value 42) { return value; }; }()); 4. name 属性 函数的 name 属性返回该函数的函数名 function foo() {} foo.name // foo 这个属性早就被浏览器广泛支持但是直到 ES6才将其写入了标准。 需要注意的是ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量ES5 的 name 属性会返回空字符串而 ES6 的 name 属 性会返回实际的函数名。 var f function () {}; // ES5 f.name // // ES6 f.name // f 上面代码中变量 f 等于一个匿名函数ES5 和 ES6 的 name 属性返回的值不一样。 如果将一个具名函数赋值给一个变量则 ES5 和 ES6 的 name 属性都返回这个具名函数原本的名字 const bar function baz() {}; // ES5 bar.name // baz // ES6 bar.name // baz Function 构造函数返回的函数实例 name 属性的值为 anonymous (new Function).name // anonymous bind 返回的函数 name 属性值会加上 bound 前缀。 function foo() {}; foo.bind({}).name // bound foo (function(){}).bind({}).name // bound 5. 箭头函数 5.1基本用法 ES6 允许使用“箭头” 定义函数。 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; }; 如果箭头函数的代码块部分多于一条语句就要使用大括号将它们括起来并且使用 return 语句返回 var sum (num1, num2) { return num1 num2; } 由于大括号被解释为代码块所以如果箭头函数直接返回一个对象必须在对象外面加上括号否则会报错。 // 报错 let getTempItem id { id: id, name: Temp }; // 不报错 let getTempItem id ({ id: id, name: Temp }); 如果箭头函数只有一行语句且不需要返回值可以采用下面的写法就不用写大括号了。 let fn () void doesNotReturn(); 箭头函数可以与变量解构结合使用。 const full ({ first, last }) first last; // 等同于 function full(person) { return person.first person.last; } 箭头函数使得表达更加简洁 const isEven n n % 2 0; const square n n * n; 上面代码只用了两行就定义了两个简单的工具函数。如果不用箭头函数可能就要占用多行而且还不如现在这样写醒目。 箭头函数的一个用处是简化回调函数。   // 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1,2,3].map(x x * x); 另一个例子是 // 正常函数写法 var result values.sort(function (a, b) { return a - b; }); // 箭头函数写法 var result values.sort((a, b) a - b); 下面是 rest 参数与箭头函数结合的例子。 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]] 5.2使用注意点 箭头函数有几个使用注意点。 1函数体内的 this 对象就是定义时所在的对象而不是使用时所在的对象。 2不可以当作构造函数也就是说不可以使用 new 命令否则会抛出一个错误。 3不可以使用 arguments 对象该对象在函数体内不存在。如果要用可以用 rest 参数代替。 4不可以使用 yield 命令因此箭头函数不能用作 Generator 函数。 上面四点中第一点尤其值得注意。 this 对象的指向是可变的但是在箭头函数中它是固定的。 function foo() { setTimeout(() { console.log(id:, this.id); }, 100); } var id 21; foo.call({ id: 42 }); // id: 42 上面代码中 setTimeout 的参数是一个箭头函数这个箭头函数的定义生效是在 foo 函数生成时而它的真正执行要等到 100 毫秒后。如果是普通函 数执行时 this 应该指向全局对象 window 这时应该输出 21 。但是箭头函数导致 this 总是指向函数定义生效时所在的对象本例是 {id: 42} 所 以输出的是 42 。 箭头函数可以让 setTimeout 里面的 this 绑定定义时所在的作用域而不是指向运行时所在的作用域。下面是另一个例子。 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(s2: , timer.s2), 3100); // s1: 3 // s2: 0 上面代码中 Timer 函数内部设置了两个定时器分别使用了箭头函数和普通函数。前者的 this 绑定定义时所在的作用域即 Timer 函数后者的 this 指向运行时所在的作用域即全局对象。所以3100 毫秒之后 timer.s1 被更新了 3 次而 timer.s2 一次都没更新。 箭头函数可以让 this 指向固定化这种特性很有利于封装回调函数。下面是一个例子DOM 事件的回调函数封装在一个对象里面 var handler { id: 123456, init: function() { document.addEventListener(click, event this.doSomething(event.type), false); }, doSomething: function(type) { console.log(Handling type for this.id); } }; 上面代码的 init 方法中使用了箭头函数这导致这个箭头函数里面的 this 总是指向 handler 对象。否则回调函数运行时 this.doSomething 这一 行会报错因为此时 this 指向 document 对象。 this 指向的固定化并不是因为箭头函数内部有绑定 this 的机制实际原因是箭头函数根本没有自己的 this 导致内部的 this 就是外层代码块的 this 。正是因为它没有 this 所以也就不能用作构造函数。 所以箭头函数转成 ES5 的代码如下。 // ES6 function foo() { setTimeout(() { console.log(id:, this.id); }, 100); } // ES5 function foo() { var _this this; setTimeout(function () { console.log(id:, _this.id); }, 100); } 上面代码中转换后的 ES5 版本清楚地说明了箭头函数里面根本没有自己的 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: 1 var t2 f().call({id: 3})(); // id: 1 var t3 f()().call({id: 4}); // id: 1 上面代码之中只有一个 this 就是函数 foo 的 this 所以 t1 、 t2 、 t3 都输出同样的结果。因为所有的内层函数都是箭头函数都没有自己的 this 它们的 this 其实都是最外层 foo 函数的 this 。 除了 this 以下三个变量在箭头函数之中也是不存在的指向外层函数的对应变量 arguments 、 super 、 new.target function foo() { setTimeout(() { console.log(args:, arguments); }, 100); } foo(2, 4, 6, 8) // args: [2, 4, 6, 8] 上面代码中箭头函数内部的变量 arguments 其实是函数 foo 的 arguments 变量。 另外由于箭头函数没有自己的 this 所以当然也就不能用 call() 、 apply() 、 bind() 这些方法去改变 this 的指向 (function() { return [ (() this.x).bind({ x: inner })() ]; }).call({ x: outer }); // [outer] 上面代码中箭头函数没有自己的 this 所以 bind 方法无效内部的 this 指向外部的 this 。 长期以来JavaScript 语言的 this 对象一直是一个令人头痛的问题在对象方法中使用 this 必须非常小心。箭头函数”绑定” this 很大程度上解决 了这个困扰。 5.3嵌套的箭头函数 箭头函数内部还可以再使用箭头函数。下面是一个 ES5 语法的多重嵌套函数。 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] 下面是一个部署管道机制pipeline的例子即前一个函数的输出是后一个函数的输入 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) // 12 如果觉得上面的写法可读性比较差也可以采用下面的写法 const plus1 a a 1; const mult2 a a * 2; mult2(plus1(5)) // 12 箭头函数还有一个功能就是可以很方便地改写 λ 演算 // λ演算的写法 fix λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v))) // ES6的写法 var fix f (x f(v x(x)(v))) (x f(v x(x)(v))); 上面两种写法几乎是一一对应的。由于 λ 演算对于计算机科学非常重要这使得我们可以用 ES6 作为替代工具探索计算机科学。 6. 双冒号运算符 箭头函数可以绑定 this 对象大大减少了显式绑定 this 对象的写法 call 、 apply 、 bind 。但是箭头函数并不适用于所有场合所以现在有一个 提案提出了“函数绑定”function bind运算符用来取代 call 、 apply 、 bind 调用。 函数绑定运算符是并排的两个冒号 :: 双冒号左边是一个对象右边是一个函数。该运算符会自动将左边的对象作为上下文环境即 this 对 象绑定到右边的函数上面。 foo::bar; // 等同于 bar.bind(foo); foo::bar(...arguments); // 等同于 bar.apply(foo, arguments); const hasOwnProperty Object.prototype.hasOwnProperty; function hasOwn(obj, key) { return obj::hasOwnProperty(key); } 如果双冒号左边为空右边是一个对象的方法则等于将该方法绑定在该对象上面 var method obj::obj.foo; // 等同于 var method ::obj.foo; let log ::console.log; // 等同于 var log console.log.bind(console); 双冒号运算符的运算结果还是一个对象因此可以采用链式写法 // 例一 import { map, takeWhile, forEach } from iterlib; getPlayers() ::map(x x.character()) ::takeWhile(x x.strength 100) ::forEach(x console.log(x)); // 例二 let { find, html } jake; document.querySelectorAll(div.myClass) ::find(p) ::html(hahaha) 7. 尾调用优化 7.1什么是尾调用 尾调用Tail Call是函数式编程的一个重要概念本身非常简单一句话就能说清楚就是指某个函数的最后一步是调用另一个函数。 function f(x){ return g(x); } 上面代码中函数 f 的最后一步是调用函数 g 这就叫尾调用。 以下三种情况都不属于尾调用。 // 情况一 function f(x){ let y g(x); return y; } // 情况二 function f(x){ return g(x) 1; } // 情况三 function f(x){ g(x); } 上面代码中情况一是调用函数 g 之后还有赋值操作所以不属于尾调用即使语义完全一样。情况二也属于调用后还有操作即使写在一行内。情况 三等同于下面的代码。 function f(x){ g(x); return undefined; } 尾调用不一定出现在函数尾部只要是最后一步操作即可 function f(x) { if (x 0) { return m(x) } return n(x); } 上面代码中函数 m 和 n 都属于尾调用因为它们都是函数 f 的最后一步操作。 7.2尾调用优化 尾调用之所以与其他调用不同就在于它的特殊的调用位置。 我们知道函数调用会在内存形成一个“调用记录”又称“调用帧”call frame保存调用位置和内部变量等信息。如果在函数 A 的内部调用函数 B 那 么在 A 的调用帧上方还会形成一个 B 的调用帧。等到 B 运行结束将结果返回到 A B 的调用帧才会消失。如果函数 B 内部还调用函数 C 那就还有一 个 C 的调用帧以此类推。所有的调用帧就形成一个“调用栈”call stack。 尾调用由于是函数的最后一步操作所以不需要保留外层函数的调用帧因为调用位置、内部变量等信息都不会再用到了只要直接用内层函数的调用 帧取代外层函数的调用帧就可以了 function f() { let m 1; let n 2; return g(m n); } f(); // 等同于 function f() { return g(3); } f(); // 等同于 g(3); 上面代码中如果函数 g 不是尾调用函数 f 就需要保存内部变量 m 和 n 的值、 g 的调用位置等信息。但由于调用 g 之后函数 f 就结束了所以执行到最 后一步完全可以删除 f(x) 的调用帧只保留 g(3) 的调用帧。 这就叫做“尾调用优化”Tail call optimization即只保留内层函数的调用帧。如果所有函数都是尾调用那么完全可以做到每次执行时调用帧只有 一项这将大大节省内存。这就是“尾调用优化”的意义。 注意只有不再用到外层函数的内部变量内层函数的调用帧才会取代外层函数的调用帧否则就无法进行“尾调用优化”。 function addOne(a){ var one 1; function inner(b){ return b one; } return inner(a); } 上面的函数不会进行尾调用优化因为内层函数 inner 用到了外层函数 addOne 的内部变量 one 7.3尾递归 函数调用自身称为递归。如果尾调用自身就称为尾递归。 递归非常耗费内存因为需要同时保存成千上百个调用帧很容易发生“栈溢出”错误stack overflow。但对于尾递归来说由于只存在一个调用帧 所以永远不会发生“栈溢出”错误。 function factorial(n) { if (n 1) return 1; return n * factorial(n - 1); } factorial(5) // 120 上面代码是一个阶乘函数计算 n 的阶乘最多需要保存 n 个调用记录复杂度 O(n) 。 如果改写成尾递归只保留一个调用记录复杂度 O(1) function factorial(n, total) { if (n 1) return total; return factorial(n - 1, n * total); } factorial(5, 1) // 120 还有一个比较著名的例子就是计算 Fibonacci 数列也能充分说明尾递归优化的重要性。 非尾递归的 Fibonacci 数列实现如下。 function Fibonacci (n) { if ( n 1 ) {return 1}; return Fibonacci(n - 1) Fibonacci(n - 2); } Fibonacci(10) // 89 Fibonacci(100) // 堆栈溢出 Fibonacci(500) // 堆栈溢出 尾递归优化过的 Fibonacci 数列实现如下 function Fibonacci2 (n , ac1 1 , ac2 1) { if( n 1 ) {return ac2}; return Fibonacci2 (n - 1, ac2, ac1 ac2); } Fibonacci2(100) // 573147844013817200000 Fibonacci2(1000) // 7.0330367711422765e208 Fibonacci2(10000) // Infinity 由此可见“尾调用优化”对递归操作意义重大所以一些函数式编程语言将其写入了语言规格。ES6 是如此第一次明确规定所有 ECMAScript 的实 现都必须部署“尾调用优化”。这就是说ES6 中只要使用尾递归就不会发生栈溢出相对节省内存。 7.4递归函数改写 尾递归的实现往往需要改写递归函数确保最后一步只调用自身。做到这一点的方法就是把所有用到的内部变量改写成函数的参数。比如上面的例 子阶乘函数 factorial 需要用到一个中间变量 total 那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观第一眼很难看出来为什么 计算 5 的阶乘需要传入两个参数 5 和 1 两个方法可以解决这个问题。方法一是在尾递归函数之外再提供一个正常形式的函数。 function tailFactorial(n, total) { if (n 1) return total; return tailFactorial(n - 1, n * total); } function factorial(n) { return tailFactorial(n, 1); } factorial(5) // 120 上面代码通过一个正常形式的阶乘函数 factorial 调用尾递归函数 tailFactorial 看起来就正常多了。 函数式编程有一个概念叫做柯里化currying意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。 function currying(fn, n) { return function (m) { return fn.call(this, m, n); }; } function tailFactorial(n, total) { if (n 1) return total; return tailFactorial(n - 1, n * total); } const factorial currying(tailFactorial, 1); factorial(5) // 120 上面代码通过柯里化将尾递归函数 tailFactorial 变为只接受一个参数的 factorial 。 第二种方法就简单多了就是采用 ES6 的函数默认值。 function factorial(n, total 1) { if (n 1) return total; return factorial(n - 1, n * total); } factorial(5) // 120 上面代码中参数 total 有默认值 1 所以调用时不用提供这个值。 总结一下递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令所有的循环都用递归实现这就是为什么尾递归对这些语言极其重 要。对于其他支持“尾调用优化”的语言比如 LuaES6只需要知道循环可以用递归代替而一旦使用递归就最好使用尾递归。 7.5严格模式 ES6 的尾调用优化只在严格模式下开启正常模式是无效的。 这是因为在正常模式下函数内部有两个变量可以跟踪函数的调用栈。 func.arguments 返回调用时函数的参数。 func.caller 返回调用当前函数的那个函数。 尾调用优化发生时函数的调用栈会改写因此上面两个变量就会失真。严格模式禁用这两个变量所以尾调用模式仅在严格模式下生效。 function restricted() { use strict; restricted.caller; // 报错 restricted.arguments; // 报错 } restricted() 7.6尾递归优化的实现 尾递归优化只在严格模式下生效那么正常模式下或者那些不支持该功能的环境中有没有办法也使用尾递归优化呢回答是可以的就是自己实现尾 递归优化。 它的原理非常简单。尾递归之所以需要优化原因是调用栈太多造成溢出那么只要减少调用栈就不会溢出。怎么做可以减少调用栈呢就是采用“循 环”换掉“递归”。 下面是一个正常的递归函数。 function sum(x, y) { if (y 0) { return sum(x 1, y - 1); } else { return x; } } sum(1, 100000) // Uncaught RangeError: Maximum call stack size exceeded(…) 上面代码中 sum 是一个递归函数参数 x 是需要累加的值参数 y 控制递归次数。一旦指定 sum 递归 100000 次就会报错提示超出调用栈的最大次 数。 蹦床函数trampoline可以将递归执行转为循环执行 function trampoline(f) { while (f f instanceof Function) { f f(); } return f; } 上面就是蹦床函数的一个实现它接受一个函数 f 作为参数。只要 f 执行后返回一个函数就继续执行。注意这里是返回一个函数然后执行该函数 而不是函数里面调用函数这样就避免了递归执行从而就消除了调用栈过大的问题。 然后要做的就是将原来的递归函数改写为每一步返回另一个函数。 function sum(x, y) { if (y 0) { return sum.bind(null, x 1, y - 1); } else { return x; } } 上面代码中 sum 函数的每次执行都会返回自身的另一个版本。 现在使用蹦床函数执行 sum 就不会发生调用栈溢出 trampoline(sum(1, 100000)) // 100001 蹦床函数并不是真正的尾递归优化下面的实现才是。 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 上面代码中 tco 函数是尾递归优化的实现它的奥妙就在于状态变量 active 。默认情况下这个变量是不激活的。一旦进入尾递归优化的过程这个变 量就激活了。然后每一轮递归 sum 返回的都是 undefined 所以就避免了递归执行而 accumulated 数组存放每一轮 sum 执行的参数总是有值的这 就保证了 accumulator 函数内部的 while 循环总是会执行。这样就很巧妙地将“递归”改成了“循环”而后一轮的参数会取代前一轮的参数保证了调用栈 只有一层 8. 函数参数的尾逗号 ES2017 允许函数的最后一个参数有尾逗号trailing comma。 此前函数定义和调用时都不允许最后一个参数后面出现逗号。 function clownsEverywhere( param1, param2 ) { /* ... */ } clownsEverywhere( foo, bar ); 上面代码中如果在 param2 或 bar 后面加一个逗号就会报错。 如果像上面这样将参数写成多行即每个参数占据一行以后修改代码的时候想为函数 clownsEverywhere 添加第三个参数或者调整参数的次序 就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说就会显示添加逗号的那一行也发生了变动。这看上去有点冗余因此新的语 法允许定义和调用时尾部直接有一个逗号。 function clownsEverywhere( param1, param2, ) { /* ... */ } clownsEverywhere( foo, bar, ); 这样的规定也使得函数参数与数组和对象的尾逗号规则保持一致了 9. catch 语句的参数 目前有一个提案允许 try...catch 结构中的 catch 语句调用时不带有参数。这个提案跟参数有关也放在这一章介绍。 传统的写法是 catch 语句必须带有参数用来接收 try 代码块抛出的错误。 try { // ··· } catch (error) { // ··· } 新的写法允许省略 catch 后面的参数而不报错。 try { // ··· } catch { // ··· } 新写法只在不需要错误实例的情况下有用因此不及传统写法的用途广。 let jsonData; try { jsonData JSON.parse(str); } catch { jsonData DEFAULT_DATA; } 上面代码中 JSON.parse 报错只有一种可能解析失败。因此可以不需要抛出的错误实例。 总结 本博客源于本人阅读相关书籍和视频总结创作不易谢谢点赞支持。学到就是赚到。我是歌谣励志成为一名优秀的技术革新人员。 欢迎私信交流一起学习一起成长。 推荐链接 其他文件目录参照 “睡服“面试官系列之各系列目录汇总建议学习收藏
http://www.zqtcl.cn/news/742860/

相关文章:

  • 安阳网站建设优化免费的免抠图素材网站
  • 网站主机有什么用seo网站课程
  • 网站关键词优化软件网站的二次开发
  • 网站建设技术服务费怎么入账杭州网站推广与优化
  • 咨询类网站建设方案书重庆360网络推广
  • 简单网站模板下载wordpress调用数据库字段
  • 万网空间最多放几个网站好的网站首页的特点
  • .net做网站安全吗wordpress取消邮件验证
  • 沈阳做网站推广唐山网站怎么做seo
  • 网站备案说主体已注销刷关键词指数
  • 学做网站教学百度网盘动软代码生成器 做网站
  • 长辛店网站建设手机评测网站
  • 网站建设公司选哪个好软件开发
  • 隐形眼镜网站开发的经济效益莘县网站开发
  • 开创集团网站建设如何在学校网站上做链接
  • 上海优秀网站设计百度投诉中心人工电话号码
  • 卖建材的网站有哪些跨境电商工具类产品的网站
  • 做毕业网站的周记网站开发项目书
  • 门户网站价格仿站工具下载后咋做网站
  • 国外优秀ui设计网站常州网站建设电话
  • 大连手机网站建设做外贸无网站如何做
  • 做旅游门票网站需要什么材料人工智能培训机构哪个好
  • 免费的网站程序个人网站可以做论坛么
  • ps中网站页面做多大的wordpress cdn 阿里
  • 深圳整站创意设计方法有哪些
  • 浙江做网站多少钱江门市网站开发
  • 保定建站价格dw软件免费安装
  • 在建设部网站上的举报凡科网怎么建网站
  • wordpress做小说网站工作期间员工花钱做的网站
  • 婚介网站方案小说网站架构