函数的拓展

7.1.1 基本用法

在ES6之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

function log(x.y){
y = y || 'World'l
console.log(x,y);
}log('hello')  //hello World
log('hello','Chine')  //hello Chine
log('hello','')  //hello World

上面的代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像以上代码的最后一行,参数y就等于空字符,结果被改为默认值。
为了避免这个问题吗,通常需要先判断一下参数y是否被赋值,如果没有,再令其等于默认值。

if(typeof y === 'undefined'){
y = 'world';
}

ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。

function Point(x = 0,y = 0){
this.x = x;
this.y = y;
}
var 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 = 1){
//...
}
// SyntaxError:

另外一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值,也就是说,参数默认值是惰性求值的。

function foo(p = x + 1){
console.log(p);
}
foo()//100
x = 100;
foo() //101
上面代码中,参数p默认值是x+1。这时,每次调用函数foo都会重新计算,而不是默认p等于100

7.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:

上面的代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值,只有当函数foo的参数是一个对象时,变量x和y才会通过解构赋值而生成。如果函数foo调用时参数不是对象,变量x和y就不会生成,从而报错。只有参数对象没有y属性时,y的默认值5才会生效。
下面是另一个对象的解构赋值默认值的例子。

function fetch(url,{ bodu = '',method = 'GET',headers = {}}){
console.log(method):
}
fetch('http://example.com',{})
//“GET”
fetch('http://example.com')
//报错

上面的代码中,如果函数fetch的第二个参数是一个对象,就可以为他的3个属性设置默认值。
上面的写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数,这时就出现了双重默认值。

function fetch(url,{method = 'GET'}={}){
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({}) //[0,0]
m2({x:3}) //[3,undefined]//x和y都无值的情况
m1({}) //[0,0]
m2({}) //[undefined,undefined]
m1({z:3}) //[0,0]
m2({z:3}) //[undefined,undefined]

7.1.3 参数默认值的位置

通常情况下,定义了默认值的参数应该是函数的尾参数。因为这样比较容易看出到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是无法省略的。

//列一
function f(x = 1,y){return [x, y];}f() //[1,undifned]f(2) //[2,undifned]f(,1) //报错f(undifned,1) //[1,1]//列一
function f(x ,y = 5,z){return [x, y,z];}f() //[undifned,5,undifned]f(1) //[1,5,undifned]f(1,2) //报错f(1,undifned,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,没有触发默认值。

7.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。
这时因为lenght属性的含义是该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括参数了。同理,rest参数也不会计入length属性。

(function (...args){}).length //0
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数。
(function (a = 0,b,c){}).length //0
(function (a,b = 1,c){}).length //0

7.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
像下面这样写,也会报错
var x = 1;
function foo(x = x){
//...
}
foo() //ReferenceError
上面的代码中,参数x=x 形成一个单独作用域,实际执行的是let x = x。由于暂时性死区,这行这行代码会产生“定义”错误。

如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。

let foo = 'outer';function bar(func = x =>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指向函数外层,但是函数外层并没有声明变量foo,所以报错。下面是一个更复杂的例子。var x= 1;function foo(x,y = function(){ x = 2;}){var x = 3y();console.log(x);}foo() //3x // 1
上面的代码中,函数foo的参数形成一个单独作用域。这个作用域中首先声明了变量x,然后声明了变量y。y的默认值是一个匿名函数,这个匿名函数内部的变量x指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。如果将var x = 3var去除,函数foo 的内部变量x就指向第一个参数x,与匿名函数内部的x 是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。var x = 1;function foo(x, y = function(){ x = 2;}) {x = 3;y();console.log(x);}foo() //2x // 1

7.1.6 应用

利用参数默认值可以指定某一个参数不得省略,如果省略就抛出一个错误。

function throwIfMissing() {
throw new Error('Missing parameter');
}
function foo(musBeProvied = throwIfMissing()){return mustBeProvided;}
foo()//Error:Missing parameter
如果调用的时候没有参数,以上代码中的foo函数就会调用默认值throwIfMissing函数,从而抛出一个错误。从上面的代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是定义是执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。另外,可以见参数默认值设为undefined,这表明这个参数是可以省略的。
function foo(optional = undefined) { ... }

7.2 rest参数

ES6引入了rest参数(形式为“…变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入其中。

function add(...values){let sun = 0;for(var val of values) {sum += val;}return sunm;
}
add(2,5,3) //10以上代码中的add函数是一个求和函数,利用rest参数可以向该函数传入任意数目的参数。下面是一个rest参数代替arguments变量的例子。//arguments变量的写法
function sortNumbers() {return Arrayt.prototype.slice.call(arguments).sort();
}
//rest 参数的写法
const sortNumbers = (...numbers) => numbers.sort();
比较上面的两种写法可以发现,rest参数的写法更自然也更简洁。

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 //0

7.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 struct';function doSomething(a,b = a) {//code}第二种是把函数包在一个无参数的立即执行函数里面。
const doSomething = (function() {'use strict'return function(value = 42) {return value;};{());

7.4 name属性

函数的name属性返回该函数的函数名。
function foo(){}
foo.name //"foo"

这个属性早就被浏览器广泛支持,但是直到ES6才写入了标准。
需要注意的是,ES6对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5的name属性会返回空字符串,而ES6的name属性会返回实际的函数名。

var f = function() {};// ES5
f.name //""
//ES6
f.name //"f"上面代码中,变量func1等于一个匿名函数,ES5ES6的name属性返回的值不一样。如果将一个剧名函数赋值给一个变量,则ES5ES6的name属性都返回这个具名函数原本的名字
const bar = function baz() {};
// ES5
var.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"

7.5 箭头函数

7.5.1 基本用法
ES6允许使用“箭头”(=>) 定义函数。

var f = v=> v;上面的箭头函数等同于以下代码。
var f = function(v) {
return v;
};如果箭头函数不需要参数或需要多个参数,就使用圆括号代表参数部分。
var f = () => 5;
//等同于
var f = function() {retrun s};var sum = (num1,num2) => num1+num2;
//等同于
var sum = function(num1,num2) {return num1 + num2;
};如果箭头函数的代码块部分多余一条语句,就要使用大括号将其括起来,并使用retrun语句返回。
var sun = (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;
}箭头函数使得表达式更简洁。
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;number(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]]

7.5.2 注意事项

箭头函数有以下几个使用注意事项。
  1. 函数体内的this对象就是定义是所在的对象,而不是使用时所在的对象。
  2. 不可以当作构造函数。也就是说,不可以使用new命令,否则会抛出一个错误
  3. 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用rest参数代替。
  4. 不可以使用yield命令,因此箭头函数不能用作Generatot函数。
    其中一点尤其值得注意。this对象的指向是可变的,但箭头函数中它是固定的,
function foo() {setTimeout(() =>{console.log('id',this.id);},100);}
var id = 21;
foo.call({ id : 42});
//id:42上面的代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义是在foo函数生成时生效的,而他真正执行要等到100ms后。如果是普通函数,执行时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指向运行时所在的作用域(即全局对象)。所以3100ms之后,thmer.s1被更新了3次,而thimer。说一次都没更新。箭头函数可以让this指向固定化,这种特性非常有利于封装回调函数。假面是一个例子。DOM事件的回调函数封装在一个对象里面。var handler = {id:'123456',init:function() {document.addEventListener('click,event => this.doSomething(event.type),false);},doSomething:function(type){console.log('Handing' + type +'for' + this.id);}};以上的代码的init方法中使用了箭头函数,这导致箭头函数里面的this总是指向handler对象。否则,回调函数运行时,this.doSomething一行会报错,因为此时this指向document对象。this指向的固定化并不是因为箭头函数内部有绑定this的机制,实际原因是因为箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为他没有this,所以不能用作构造函数。箭头函数转成ES5的代码如下。
//ES5
function foo() {setTimeout(() => {console.log('id:',this.id);},100);
}//ES5
function foo() {var _this = thislsetTimeout(function () {console.log('id:',_this.id);},100);
}上面的代码中,转换后的ES5版本清楚地说明了箭头函数里面根本没有自己的this,而是引用外层的this。请问下面的代码之中有几个thisfunction foo() [return ()=> {retrun () => {retrun () => {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,就是函数foo的this,所以t1,t2,t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,他们的this其实就是最外层foo函数的this。除了this,以下3个变量在箭头函数中也是不存在的,分别指向外层函数的对应变量:arguments,supernew.targetfunction foo() {setTimeout(() => {console.log('args:',arguments);},100);}
foo(2,4,5,8)
//args:[2,4,6,8]上面的代码中,箭头函数内部的变量arguments其实就是函数foo的arguments变量。另外,由于箭头函数没有自己的this,当然也就不用call(),apply(),bind()这些方法去改变this的指向。
(function() {retrun [(() => this.x).bind({ x: 'inner' })()];}).call({ x:'outer'});//['outer']上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this只想外部的this。长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this必须非常小心。箭头函数“绑定”this,很大程度上解决了这个困扰。

7.5.3 嵌套的箭头函数

箭头函数内部还可以在使用箭头函数。下面是一个ES5语法的多重嵌套函数。
function insert(value) {return {into:function (array) {return {after:function  (afterValue) }array.splice(arrat.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

7.6 绑定this

箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call,apply,bind).但是,箭头函数并非适用于所有场合,所以ES7提出“函数绑定”(function bind )运算符,用来取代call,apply,bind调用。
函数绑定运算符是并排的双冒号( ::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象作为上下文环境(即this对象)绑定到右边函数上

	foo::bar;//等同于bar.bind(foo)foo::bar(...arguments);//等同于bar.apply(foo,arguments);
const hasOwnPropery = 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 尾调用优化

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)}retrun n(x);
}
上面的代码中,函数m和n都属于尾调用,因为他们都是函数f的最后一步操作。

7.7.2 尾调用优化
尾调用之所以与其他调用不同,就在于其特殊的调用位置。
我们知道,函数调用会在内存形成一个**“调用记录”又称“调用帧”**,保存调用位置和内存变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧就形成一个“调用栈“
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置内部变量等信息都不回再用到了,直接用内层函数的调用帧取代外层函数即可。

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)的调用帧,
这就叫做“尾调用优化”,即只保留内层函数的调用帧,如果所有的函数都是尾调用,那么完全可以做到每次执行时调用帧只有一项,这将大大节省内存,这就是“尾调用优化”的意义。
**注意**
只有不在用到外层函数的内部变量,内层函数的调用帧才回取代外层函数的调用帧,否则就无法进行“尾调用优化”.
function addDne(a){var one = 1;funciton inner(b) {return b+one;}return inner(a);
}
上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。

7.7.3 尾递归

函数调用自身称为递归,如果尾调用自身就称尾递归。
递归非常耗费内存,因为需要同时保存成百上千个调用帧。很容易发生栈溢出错误。但对于尾递归来说由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
function factcrial(n) {if(n=== 2) return 1;return n * factoral(n - 1);
}
factorial(5)// 120上面的代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度尾O(n).
如果改写成尾调用递归,只保留一个调用记录,则复杂度尾O(n);function factorial (n,total) {if (n === 1) return total;return factorial(n = 1,n* total);
}
factorial(5,1) //120

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/15088.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

UE5 双手握剑的实现(逆向运动学IK)

UE5 双手握剑的实现 IK 前言 什么是IK? UE官方给我们提供了很多对于IK处理的节点,比如ABRIK、Two Bone IK、Full Body IK 、CCD IK等,但是看到这,很多人就好奇了,什么是IK? 首先我们来看看虚幻小白人的骨…

[图解]产品经理创新之阿布思考法

0 00:00:00,000 --> 00:00:01,900 那刚才我们讲到了 1 00:00:02,730 --> 00:00:03,746 业务序列图 2 00:00:03,746 --> 00:00:04,560 然后怎么 3 00:00:05,530 --> 00:00:06,963 画现状,怎么改进 4 00:00:06,963 --> 00:00:09,012 然后改进的模式…

【Spring Security + OAuth2】授权

Spring Security OAuth2 第一章 Spring Security 快速入门 第二章 Spring Security 自定义配置 第三章 Spring Security 前后端分离配置 第四章 Spring Security 身份认证 第五章 Spring Security 授权 第六章 OAuth2 文章目录 Spring Security OAuth21、基于request的授权1…

一条命令安装Metasploit Framework

做安全渗透的人都或多或少的使用kali-Linux系统中msfconsole命令启动工具,然而也经常会有人遇到这样那样的问题无法启动 今天我们就用一条命令来重新安装这个工具 curl https://raw.githubusercontent.com/rapid7/metasploit-omnibus/master/config/templates/met…

AI学习AI知识路线

数学基础 一、数据分析 二、概率论 三、线性代数及矩阵 l 数学基础 1)常数e2)导数3)梯度 4)Taylor5)gini系数6)信息熵与组合数 1)概率论基础2)古典模型3)常见概率分布 4)大数定理和中心极限定理5)协方差(矩阵)和相关系数 6)最大似然估计和最大后验估计 1)线性空间及线性变…

Windows内核--内存区对象(Section Object)(5.2)

内存区对象 Section Object表示可以共享的内存段。进程可以使用Section与其他进程共享其部分内存地址空间. Section还可为进程提供将文件映射到其内存地址空间的机制。 Linux有mmap与之类似。 参考: Section Objects and Views 内存区对象是虚拟描述符表VAD节点的一种 VAD树节点…

LabVIEW如何确保自动化设备的稳定性和可靠性?

为了确保LabVIEW在自动化设备中的稳定性和可靠性,可以采取以下关键措施: 1. 代码架构与设计 模块化设计:将程序分解为独立的模块或子VI,每个模块负责特定功能,便于测试和维护。状态机架构:使用状态机架构…

zookeeper选主之LeaderLatch

概述 利用zookeeper来进行选主,可以使用apache curator framework,它给我们封装了两种选主工具,它们分别是LeaderSelector和LeaderLatch。它们各自的应用场景不一样,LeaderSelector应用于那些需要频繁变主的情况,而Le…

Redis机制-Redis互斥锁、分布式锁

目录 一 互斥锁 二 分布式锁 Redis实现分布式锁 redisson实现分布式锁 可重入性: 主从一致性(性能差): 一 互斥锁 假设我们现在有一个业务要实现秒杀优惠券的功能,如果是一个正常的流程,线程之间应该…

数据结构中链表的题目

题目: 设计一个算法,要求将链表中所有节点的链接方向“原地”逆转,即要求仅利用原表的存储空间。 对于这个问题,首先要分析的是:链表中的头和尾节点如何插入?其次就是:如何链接? 搞懂…

阅读笔记——《未知协议状态机推断技术研究综述》

【参考文献】盛嘉杰, 牛胜杰, 陈阳, 等. 未知协议状态机推断技术研究综述[J]. 计算机与现代化, 2023 (05): 58.【注】本文仅为作者个人学习笔记,如有冒犯,请联系作者删除。 摘要 协议逆向工程(PRE)描述了协议的行为逻辑&#xff…

spring cloud config server源码学习(一)

文章目录 1. 注解EnableConfigServer2. ConfigServerAutoConfiguration2.1 ConditionalOnBean和ConditionalOnProperty2.2 Import注解2.2.1. EnvironmentRepositoryConfiguration.class2.2.2. CompositeConfiguration.class2.2.3. ResourceRepositoryConfiguration.class2.2.4.…

python3 + selenium webdriver自动化测试启动不同浏览器

selenium webdriver自动化测试启动不同浏览器 selenium webdriver 介绍Selenium WebDriver 进行自动化测试的一般流程浏览器驱动下载浏览器驱动的安装chrome、edge、Firefox、Opera、Safari、phantomjs 应用Headless Chrome 、Headless Firefox 应用 selenium webdriver 介绍 …

shell命令运行原理及Linux权限问题

目录 shell命令以及运行原理用户管理添加用户删除用户sudo Linux权限的概念Linux权限管理文件访问者的分类(人)文件类型和访问权限(事物属性)文件权限值的表示方法文件访问权限的相关设置方法 目录的权限粘滞位 shell命令以及运行…

备考AMC8和AMC10竞赛,吃透2000-2024年1850道真题和解析(持续)

多做真题,吃透真题和背后的知识点是备考AMC8、AMC10有效的方法之一,通过做真题,可以帮助孩子找到真实竞赛的感觉,而且更加贴近比赛的内容,可以通过真题查漏补缺,更有针对性的补齐知识的短板。 今天我们继续…

PostgreSQL基本使用Schema

参考文章:PostgreSQL基本使用(3)Schema_pg数据库查询schema-CSDN博客 PostgreSQL 模式(Schema)可以理解为是一个表的集合(或者所属者)。 例如:在 MySQL 中,Scheam 是库&…

gcc源码分析(AST抽象语法树)

文章目录 三、AST相关1、AST(抽象语法树)1.1 树结点的声明1.2 树结点的结构1.2.1 tree_node联合体1.2.2 tree_base结构体1.2.3 tree_common结构体1.2.4 常量结构体1.2.5 **标识符节点**2、符号绑定,作用域与block树节点2.1 lang_identifier结构体2.2 c_binding结构体2.3 scop…

HLS视频加密,让您的视频内容更安全!

背景介绍 HLS视频加密是一种基于HTTP Live Streaming(HLS)协议的加密技术。它的核心思想是将视频切片进行加密处理,在客户端播放时需要先获取解密密钥才能正常偶发。通过这种方式,HLS加密可以有效防止未经授权的第三方窃取视频内…

测试短信推荐参考

短信测试参考 国外: smstome 支持多个国家号码 官网地址: https://smstome.com/ quackr.io 支持多个国家号码 官网地址: https://quackr.io/ receive-smss 支持多个国家号码 地址: https://receive-smss.com/ receive-sms-fr…

C#字典的常用方法

C#的字典(Dictionary)类是一个通用的集合类,它实现了键值对的存储和访问。以下是一些常用的字典方法: Add(key, value):向字典中添加一个指定的键值对。Remove(key):从字典中移除具有指定键的元素。Contai…