js函数
基础
在js中,函数其实是对象。每个函数都是Function类型的实例,Function也有自己的属性和方法。因为函数是对象,所以函数名其实就是指向对象的指针,甚至和这个对象没有强制绑定。
定义函数的一些方式:
直接定义function sum(num1, num2)
{
return num1 + num2;
}
匿名定义,定义一个函数,然后把函数指针给sum变量let sum = function(num1, num2)
{
return num1 + num2;
};
通过箭头,可以称作箭头函数。这是一种语法糖,实际上和上面那个作用是一样的let sum (num1, num2) => { return num1 + num2;};
箭头函数可以用在任何使用函数表达式的地方(不需要函数名的地方),但是箭头函数和普通函数还是有一些微妙的差别的,例如不能使用arguments等。
最后一种方法时使用Function构造函数,这个函数接收任意多个参数,最后一个参数会被当成函数体,例如let sum = new Function("num1", "num2", "return num1 + num2");
.但是这种方法不推荐使用,因为他会被解释两次:第一次是解析new,第二次是解析函数。
函数和var类似,都会自动把声明提升到顶部,因此下列代码是可以的console.log(sum(10, 10));
function sum(num1, num2)
{
return num1 + num2;
}
函数名和参数
因为函数名是指向函数的指针,所以他们和其他对象指针相同,这样就意味着函数可以有多个名称。function sum(num1, num2)
{
return num1 + num2;
}
consol.log(sum(10, 10));//20
let anothersum = sum;
console.log(anothersum(10, 10));/20
所有函数对象都只会暴露一个只读的name属性,包含函数的信息。多数情况下就是包含一个函数标识符,也就是函数名。如果它是使用构造函数创建的,则会被标识成‘anonymous’
如果函数是get、set或者使用bind()实例化,那么标识符前面会加上一个前缀function foo(){}
console.log(foo.bind(null).name);//bound foo
let dog =
{
years: 1,
get age()
{
return this.years;
}
set age(newAge)
{
this.years = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(propertyDescriptor.get.name);//get age
console.log(propertyDescriptro.set.name);//set age
参数
ECMAScript函数的参数和大多数语言不同,它不关心传入参数的个数。定义函数时要接受两个参数,但是实际上可以传一个、两个、三个或者一个不传,都不会报错。
函数的参数在内部表现为一个数组,,函数调用时总会接收这个参数数组,但是数组中有什么其实并不关心。并且在使用function定义非箭头函数时,可以在函数内部访问arguments对象,从中获得传进来的每个参数值。
arguments对象类似于数组,可以通过arguments[0]获得第一个参数,arguments[1]获得第二个参数。可以使用arguments.length获得数组的长度function sayHi()
{
console.log('Hello ' + arguments[0] + ", " + arguments[1]);
}
sayHi('你', '好');
这和c语言中main函数的参数类似,length相当于argc,而这个数组相当于argv
我们可以修改arguments参数的值甚至添加元素,但是arguments是根据传入时参数的个数决定的,不会再改变fucntion add(num1, num2)
{
arguments[1] = 10;
console.log(arguments[0] + num2);
}
console.log(add(1, 3);//11
但是在严格模式下即使把arguments的值改变了参数的值也不会改变
箭头函数中的参数
箭头函数中的参数不能使用arguments进行访问,只能使用参数名进行访问function foo()
{
console.log(arguments[0]);
}
foo(5);//5
let bar = ()=>{console.log(arguments[0]);};
bar(5);//ReferenceError
js的函数没有重载,因为参数都是不确定的。在这种情况下,后命名的函数会覆盖前面一个函数
默认参数与扩展参数
例如:function makeKing(name = 'henry')
{
return `King ${name} VIII`;
}
console.log(makeKing('Louis'));
console.log(makeKing();//King henry VIII
传undefined相当于没有传值,但是通过传undefined可以部分填充function makeKing(name = 'Henry', numerals = 'VIII')
{
return `King ${name} ${numerals}`;
}
console.log(makeKing(undefined, 'VI');
在使用默认参数时,arguments不会反映默认参数,他只会保存外来参数。
默认参数还可以直接传一个函数,并且在函数调用时相应的参数函数才会求值,例如:let num = ['I', 'II', 'III'];
let ordinality = 0;
function getNumerals()
{
return num[ordinality++];
}
function makeKing(name = 'Henry', numerals = getNumerals())//使用函数
{
...
}
默认参数的定义也是有先后顺序的,后面定义的可以引用前面定义的。例如:function makeKing(name = 'Henry', numerals = name)//引用name
{
return `King ${name} ${numerals}`;
}
它类似于
function makeKing(name, numerals)
{
let name = 'Henry';
let numerals = name;
return `King ${name} ${numerals}`;
}
扩展参数
扩展参数也就是不定长参数,它在不同环境下有不同的用法。
传入参数
当参数是一个数组时,实际上它的长度是知道的,因此它的作用是将数组中的元素进行拆分,每个元素都是输入的一个参数,可以使用arguments进行访问。
let values = [1, 2, 3, 4];
function getSum()
{
let sum = 0;
for(let i=0; i<arguments.length; i++)
{
sum += arguments[i];
}
return sum;
}
可以使用
console.log(getSum(...values));//10
console.log(getSum(1, ...values)); //11
console.log(getSum(1, ...values, 2));//13输入不定长参数
例如function getSum(...values)
{
return values.reduce((x, y) => x+y, 0);
}
console.log(getSum(1, 2, 3));//6通过这种方法得来的values是一个数组,可以通过数组访问其中的每个输入参数,这种方法可以用于箭头函数(箭头函数中不能用arguments),并且在arguments中每个参数还是独立的,上面
arguments.length
输出为3不定长参数只能放在最后
function getProduct(...values, lastValue){}//错误
function getProduct(firstValue, ...values){}//可以函数的参数与属性
参数
arguments
前面已经使用过很多次arguments属性了,它代表传进来的参数。除了这个作用外,arguments其实还包含一个callee属性,是一个指向所在函数的指针。
this
在标准函数中,this引用的是调用函数的上下文对象,例如:window.color = 'red';
let o =
{
color: 'blue';
}
function sayColor()
{
console.log(this.color);
}
sayColor();//red,这时它的上下文对象时window
o.sayColor = sayColor;
o.sayColor();//blue
而在箭头函数中,this引用的是定义时的上下文window.color = 'red';
let o =
{
color: 'blue';
}
let sayColor = () => console.log(this.color);
sayColor();//red
o.sayColor = sayColor;
o.sayColor();//red,因为是在window中定义的
函数名只是保存指针的变量,实际上函数都在代码区,只不过执行的上下文不同
caller
caller引用调用该函数的函数,例如function outer()
{
inner();
}
function inner()
{
console.log(inner.caller);
}
outer();
结果返回outer的代码,因为inner.caller是outer
new.target
new.target是为了检测这个函数是作为创建新对象而new的还是作为普通函数,如果是作为普通函数,那么new.target值是undefined,如果是new,那么将引用被调用的函数function King()
{
if(!new.target)
{
throw `King must be instantiated using "new"`
}
console.log(`King instantiated using "new"`);
}
new King();//King instantiated using "new"
King();//Error: King must be instantiated using "new"
属性和方法
每个函数都有两个属性:length和prototype,其中length是函数定义的参数个数function sayName(name)
{
console.log(name);
}
function sum(num1, num2)
{
return num1 + num2;
}
console.log(sayName.length);//1
console.log(sum.length);//2
函数有两个方法:apply()和call()。
- apply(this, arguments): 第一个参数是函数调用时的上下文对象,也就是this所指向的值,第二个参数可以是Array,也可以是arguments对象
function sum(num1, num2)
{
return num1 + num2;
}
function callsum(num1, num2)
{
return sum.apply(this, arguments);//传入arguments对象
}
console.log(callsum(10, 10));//20 - call(this, arguments): 作用和apply相同,不同的是call中的参数必须一个个列出来,而不能传数组
apply和call主要是用来切换this对象用的,例如window.color = 'red';
let o =
{
color: 'blue'
};
function sayColor()
{
console.log(this.color);
}
sayColor();
sayColor.call(this);//red
sayColor.call(window);//red
sayColor.call(o);//blue
- bind(this): 创建一个新的函数实例,并且将实例内部的this与传入的this对象进行绑定。例如:
window.color = 'red'; |
递归
递归就是自己调用自己,一般由递归表达式和终止条件。js写递归时有它自己的特色
function factorial(num) |
递归的一大缺点就是内存占用高,而js新增一项内存管理机制(尾调用优化)可以在条件满足的时候重用栈帧
例如:function outer()
{
return inner();
}
原来它的调用过程是这样的
- 执行outer,第一个栈帧生成
- 发现返回inner,计算inner,并且第二个栈帧生成
- 计算完inner,第二个栈帧退出,然后返回给outer,第一个栈帧退出
现在是:
- 执行outer,第一个栈帧生成
- 发现inner,但是此时发现outer返回值和inner返回值相同,弹出outer栈帧
- 计算inner并返回
尾调用优化的条件:
- 在严格模式下执行
- 外部函数的返回值是对尾调用函数的调用(必须是在return中调用)
- 尾调用函数返回后不用执行额外的逻辑(如返回值加减等)
- 尾调用函数没有引用外部函数中变量
例如:function outer()
{
let foo = 'bar';
function inner(){return foo;}
return inner();//闭包,没有优化
}
function outer()
{
return inner().toString();//执行了额外的逻辑
}
function outer(a, b)
{
if(a < b)
{
return a;
}
else
{
return inner(a + b);//有优化,前面一个return可以提前得知
}
}
闭包
闭包是一个函数引用了另一个函数中的变量,这通常是在嵌套函数中实现的。例如function createComp(propertyName)
{
return function(object1, object2)
{
let value1 = object1[propertyName];//引用了外部变量
let value2 = object2[propertyName];
if(value1 < value2)
{
return -1;
}
else if(vlaue1 > value2)
{
return 1;
}
else
{
return 0;
}
};
}
前面已经说过作用域链
。在函数执行时,每个函数都有一个包含其变量的对象。在全局上下文中叫变量对象,在局部上下文叫活动对象,它会随着函数的销毁而销毁。
例如function compare(value1, value2)
{
if(value1 < value2)
return -1;
else if(value1 > value2)
return 1;
else
return 0;
}
let result = compare(5, 10);
这个例子中在定义compare函数时,会为他创建作用域链,并且预装载全局变量对象,保存在Post not found: Scope中。在调用这个函数时,会复制Post not found: Scope来创建作用域,并且将自己的活动对象放在作用域链的前端。
而在createComp中,里面的匿名函数引用了createComp中的变量,因此当他返回之后createComp并不会销毁,只有在匿名函数引用清除之后才会被销毁let compare = createComp('name');
let result = compare({name: 'a'}, {name: 'b'});
compare = null//清除引用,现在createComp才会被销毁
闭包中的this对象
window.identity = 'The Window'; |
结果返回按常理说应该是object,但是却是window。考虑对象的生成过程,其中有一步就是把this变量赋给函数,但是没有赋给函数的函数,也就是说闭包内部的this是原始window。如果我们想要让他的this是对象,可以let object =
{
identity: 'object',
getIdentityFunc()
{
let that = this;
return function()
{
return that.identity;
};
}
}
私有变量
例如:function Person(name)
{
this.getName = function()
{
return name;
};
this.setName = function(value)
{
name = value;
};
}
let person = new Person('a');
console.log(person.getName());//a
上面的name实际上是一个私有变量,因为它的作用域是在函数内部的,可以通过参数名或arguments在函数中访问,但是函数外部无法访问,只有通过get和set才可以访问。
静态私有变量
上面那种是每个实例都有的私有变量,有时候需要所有实例共享私有变量,也就是静态私有变量
(function() |
通过这种方式可以让所有person实例修改一个name属性,由于Person是闭包,所以这个匿名函数调用之后并不会销毁。