函数实际上是对象,每个函数都是Function的实例,而Function也有属性和方法,和其他引用类型一样。
函数定义方式
函数名就是指向函数对象的指针,而且不一定与函数本身紧密绑定。
函数声明方式【通常】
function sum(num1, num2){
return num1+num2;
}
函数定义最后没有分号。
函数表达式
let sum = function(num1, num2){
return num1+num2;
};
此时function关键字后面没有名称,这个函数通过sum变量来引用。这里函数末尾是有分号的,与任何变量初始化语句一样。
“箭头函数”方式
let sum = (num1, num2) => {
return num1+num2;
};
使用Function构造函数【不推荐使用】
let sum = new Funtion("num1", "num2", "return num1+num2");
可接收多个参数,最后一个函数始终是函数体,之前的参数都是函数的参数。
箭头函数
ES6新增使用胖箭头(=>)语法定义函数表达式的能力。任何使用函数表达式的地方,都可以使用箭头函数。
非常适合嵌入函数的场景
let ints = [1,2,3];
console.log(ints.map(function(i){ return i+1; })); // [2,3,4]
console.log(ints.map((i) => { return i+1; })); // [2,3,4]
若只有一个参数,可以不用括号。如果没有参数或多个参数,则需使用括号。
箭头函数不能使用arguments、super和new.target,也不能用作构造函数,也没有prototype属性。
函数名
一个函数可以有多个名称,因为函数名就是指向函数的指针,所以它们跟其它包含对象指针的变量具有相同的行为。
function sum(num1, num2){
return num1+num2;
}
console.log(sum(10, 10)); // 20
let anothor = sum;
console.log(another(10, 10)); // 20
sum = null;
console.log(sum(10, 10)); // 20
使用不带括号的函数名会访问函数指针,而不会执行函数。 把sum设置为null之后,就切断了它与函数之间的关联,而another()还是可以照常调用。
ES6的所有函数对象都暴露了一个只读的name属性,多数情况下,保存的是一个函数标识符,或者是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。 如果它使用Function构造函数创建的,则会标识成”anonymous“。
function f1() {}
let f2 = function(){};
let f3 = () => {};
console.log(f1.name); // f1
console.log(f2.name); // f2
console.log(f3.name); // f3
console.log((() => {}).name); // (空字符串)
console.log((new Function()).name); // anonymous
如果函数是一个设置函数、获取函数,或者使用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 pd = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(pd.get.name); // get age
console.log(pd.set.name); // set age
理解参数
ES函数的参数与大多数其它语言不同,既不关心传入参数的个数,也不关心这个参数的数据类型。
主要因为ES函数的参数在内部表现为一个数组。事实上,在使用function定义(非箭头)函数时,可以在函数内部访问arguments对象,从中取得传进来的每个参数值。
arguments对象是一个类数组对象(但不是Array的实例),可以使用中括号来访问其中的元素(如第一个参数为arguments[0]),若要确定传进来参数的个数,可以访问arguments.length属性。
把函数重写成不声明参数也可以:
function sayHi(){
console.log("Hello "+ arguments[0] +","+ argments[1]);
}
ES函数的参数并不是必须写出来的。 与其他语言不同,在ES中的命名参数不会创建让之后的调用必须匹配的函数签名,因为根本不存在验证命名参数的机制。
arguments对象可以跟命名参数一起使用:
function doAdd(num1, num2){
if(arguments.length === 1){
console.log(num1 + 10); // 命名参数num1保存着与arguments[0]一样的值,使用谁都无所谓。
}else if(arguments.length === 2){
console.log(arguments[0] + num2);
}
}
arguments对象的值会与对应的命名参数同步。
function doAdd(num1, num2){
arguments[1] = 10;
console.log(arguments[0] + num2);
}
上面例子,修改了arguments[1]也会修改num2的值。并不意味着访问同一个内存地址,它们在内存中还是分开的,只不过是保持同步而已。
若只传一个参数,然后设置arguments[1]为某个值,则这个值不会反映到第二个命名参数,因为arguments对象的长度是传入的参数个数,而不是定义函数时的命名参数个数确定的。
对于命名参数而言,若调用函数时没有传这个参数,则这个值为undefined。 严格模式下,若给arguments[1]赋值,不会影响到num2的值。在函数中尝试重写arguments对象会导致语法错误。(代码也不会执行)
箭头函数中的参数 不能使用arguments关键字访问,而只能通过定义的命名参数访问。
可以在包装函数中把它提供给箭头函数:
function foo(){
let bar = () =>{
console.log(arguments[0]); // 5
};
bar();
}
foo(5);
ES中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递,则传递的值就是这个对象的引用。
没有重载
ES函数没有签名,因为参数是由包含0或多个值的数组表示的。没有函数签名,自然也没有重载。
若ES中定义了两个同名函数 ,则后面定义的会覆盖先定义的。 把函数名当作指针有助于理解为什么ES没有函数重载。
let addNum = function(num){
return num+100;
};
addNum = function(num){
return num-100;
};
let result = addNum(200); // 100
默认参数值
在ES5.1及之前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined。 如果是则意味着没有传这个参数,则给它赋一个值。
ES6之后,支持显式定义默认参数。只要在函数定义中的参数后面用=就可以为参数赋一个默认值。
function makeKing(name = 'Henry'){
return 'King $(name) VIII';
}
给参数传undefined相当于没有传值,不过这样可以利用多个独立的默认值。
在使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。 与ES5严格模式一样,修改命名参数也不会影响arguments对象,始终以调用函数时传入的值为准。
function makeKing(name = 'Henry'){
name = 'CLN';
return 'King $(arguments[0])';
}
console.log(makeKing()); // 'King undefined'
console.log(makeKing('JK')); // 'King JK'
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。
function makeKing(name = 'Henry', numerals = getNumerals()){
return 'King $(name) $(numerals)';
}
函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。计算默认值的函数只有在调用函数但未传相应参数时才会被调用。
箭头函数同样也可使用默认参数,只不过在只有一个参数时,必须使用括号。
参数初始化顺序遵循”暂时性死区“规则,即前面定义的参数不能引用后面定义的。 参数也存在于自己的作用域中,它们不能引用函数体的作用域。
// 调用时不传第二个参数会报错
function makeKing(name = 'cln', numerals = defaultNum){
let defaultNum = 'ffzf';
return 'King $(name) $(numerals)';
}
参数扩展与收集
ES6新增了扩展操作符,既可以用于调用函数时传参,也可以用于定义函数参数。
扩展参数
使用扩展操作符可以将数组直接传给函数:
getSum(...values);
因为数组长度已知,可以在扩展操作符的前面或后面再传递其它参数:
countArg(-1, ...values);
countArg(-1, ...values, 5);
countArg(-1, ...values, ...[5,8,1]);
arguments对象只是消费扩展操作符的一种方式,在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,或者使用默认参数。
收集参数
可以使用扩展操作符把不同长度的独立参数组合成一个数组。
收集参数的前面如果还有命名参数,则只会收集其余的参数。若没有就会得到空数组。因为收集参数的结果可变,所以只能把收集参数作为最后一个参数。
function collect(firstValue, ...values){
console.log(values);
}
collect(); // []
collect(1); // []
collect(1,2); // [2]
collect(1,2,3); // [2,3]
箭头函数支持收集参数的定义方式。
let getSum = (..values) => {
return values.reduce((x,y) => x+y, 0);
}
console.log(getSum(1,2,3)); // 6
使用收集参数并不影响arguments对象,仍然反映调用时传给函数的参数。
function getSum(...values){
console.log(arguments.length); // 3
console.log(arguments); // [1,2,3]
console.log(values); // [1,2,3]
}
console.log(getSum(1,2,3));
函数声明与函数表达式
事实上,JavaScript引擎在加载数据时对它们是区别对待的。 JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。
console.log(sum(10,10)); // 20
function sum (num1, num2){
return num1+num2;
}
函数声明会在任何代码执行之前先被读取并添加到执行上下文,这个过程叫做函数声明提升。
而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
console.log(sum(10,10)); // 会报错
let sum =function(num1, num2){
return num1+num2;
};
上面代码出错,是因为这个函数定义包含在一个变量初始化语句中,而不是函数声明中。如果没有执行到这个语句,则执行上下文中没有函数的定义。 使用var关键字也会碰到同样的问题。
函数作为值
因为函数名在ES中就是变量,所以函数可以用在任何可以使用变量的地方。 意味着可以把函数作为参数传给另一个函数,而且还可以在一个函数中返回另一个函数。
function callFunc(someFunc, someArg){
return somFunc(someArg);
}
function add10(num){
return num+10;
}
let result = callFunc(add10, 10);
console.log(result); // 20
callFunc函数是通用的,第一个参数传入的是任何函数,始终返回调用作为第一个参数传入的函数的结果。 如果是访问函数而不是调用函数,就必须不带括号。 因此传给callFunc()必须是add10,而不是它的执行结果。
从一个函数中返回另一个函数也是可以的。
function compareFunc(propertyName){
return function(obj1, obj2){
let value1 = obj1[propertyName];
let value2 = obj2[propertyName];
if(value1 > value2){
return -1;
}else if(value1 < value2){
return 1;
}else{
return 0;
}
};
}
let data = {
{name : "jk", age : 24},
{name : "cln", age : 21}
};
data.sort(compareFunc("name"));
console.log(data[0].name); // cln
data.sort(compareFunc("age"));
console.log(data[0].name); // cln
函数内部
在ES5中,函数内部存在两个特殊的对象:arguments和this。ES6又新增了new.target属性。
arguments
是一个类数组对象,包含调用参数时传入的所有参数。这个对象只有以function关键字定义函数(相对于使用箭头语法创建函数)时才会有。 还有一个属性callee,是一个指向arguments对象所在函数的指针。
// 经典阶乘函数
function factorial(num){
if(num <= 1){
return 1;
}else{
// return num * factorial(num-1); // 紧密耦合
return num * arguments.callee(num-1); // 可以让函数逻辑与函数名解耦
}
}
已经用arguments.callee代替了之前硬编码的factorial。无论函数叫什么名称,都可以正确引用函数。
let trueFactoral = factorial;
factorial = function(){
return 0;
};
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
this
它在标准函数和箭头函数中有不同的行为。
在标准函数中,this引用的是把函数当成方法调用的上下文对象,通常称其为this值(在网页的全局上下文中调用函数时,this指向windows)。
window.color = "red";
let o = { color : "blue" };
function sayColor(){
console.log(this.color);
}
sayColor(); // 'red',在全局上下文中调用,this指向window
o.sayColor = sayColor;
o.sayColor(); // 'blue',this指向o
在箭头函数中,this引用的是定义箭头函数的上下文。
window.color = "red";
let o = { color : "blue" };
let sayColor = () => console.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red',,在window上下文中定义的箭头函数,this指向window
在事件回调或定时回调中调用某个函数时,this值指向的并没想要的对象。此时可以将回调函数写成箭头函数可以解决问题,因为箭头函数中的this会保留定义该函数时的上下文。
function King(){
this.royaltyName = 'Henry';
// this 引用King的实例
setTimeout(() => console.log(this.royaltyName), 1000);
}
new King(); // Henry
函数名只是保存指针的变量,因此全局定义的sayColor()函数和o.sayColor()是同一个函数,,只不过执行的上下文不同。
caller
ES5也会给函数对象上添加一个属性: caller,引用的是调用当前函数的函数,或者如果是全局作用域中调用的则为null。
function outer(){
inner();
}
function inner(){
// console.log(inner.caller); // inner.caller指向outer()
console.log(arguments.callee.caller); // 降低耦合度
}
outer();
在严格模式下访问arguments.callee会报错。
ES5也定义了arguments.caller,严格模式下访问报错,同时不能给函数的caller属性赋值,否则会导致错误;非严格模式下始终是undefined。
new.target
ES6新增了检测函数是否使用new关键字调用的new.target属性。
如果函数是正常调用,则new.target的值为undefined;
若是使用new关键字调用的,则new.target将引用被调用的构造函数。
function King(){
if(!new.target){
throw 'King must be instantiated using "new"';
}else{
console.log('King instantiated using "new"');
}
}
new King(); // King instantiated using "new"
King(); // Error : King must be instantiated using "new"
函数属性与方法
每个函数都有两个属性:length和prototype,length属性保存函数定义的命名参数的个数。
prototype是保存引用类型所有实例的地方,在ES5中,prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。
函数有两个方法:apply()和call(),都会以指定的this值来调用函数,即会设置调用函数时函数体内this对象的值。
apply()接收两个参数:函数体内this值和一个参数数组,第二个参数可以是Array的实例,或者arguments对象。
sum.apply(this, arguments);
sum.apply(this, [num1, num2]);
在严格模式下,调用函数时如果没有指定上下文对象,则this值不会指向window。除非 使用apply()或call()把函数指定给一个对象,否则this值会变成undefined。
call()与apply()作用一样,只是传参方式不同。第一个参数和apply()一样,即this值,而剩下的要传给被调用函数的参数是逐个传递的。
两种方法真正强大的地方,是控制函数调用上下文,即函数体内this值的能力。
window.color = 'red';
let o = { color : 'blue' };
function sayColor(){
console.log(this.color);
}
sayColor(); // red,在全局上下文中调用,this指向window
sayColor(this); // red
sayColor(window); // red
sayColor(o); // blue
使用apply()或call()的好处是可以将任意对象设置为任意函数的作用域。 前面的例子,为切换上下文需要先把sayColor()直接赋值为o的属性,然后再调用。 而在修改后的版本中,就不需要这一步操作了。
ES5定义bind(),会创建一个新的函数实例,其this值会被绑定到传给bing()的对象。
window.color = 'red';
let o = { color : 'blue' };
function sayColor(){
console.log(this.color);
}
let objSayColor = sayColor.bind(o);
objSayColor(); // blue
对函数而言,继承的方法toLocaleString()和toString()始终返回函数的代码,而valueof()返回函数本身。
函数表达式
创建一个函数再把它赋值给一个变量,这样创建的函数叫做匿名函数(又称兰姆达函数),因为function关键字后面没有标识符。
理解函数声明与函数表达式的区别,关键是理解提升。
if(condition){
function sayHi(){
console.log('Hi!');
}
}else{
function sayHi(){
console.log('Yo!');
}
}
以上这段代码看起来正常,但是这种写法在ES并不是有效的写法,JS引擎会尝试将其纠正为适当的声明。 但是有些浏览器纠正这个问题的方式不一致。因此这种写法很危险,不要使用。
let sayHi():
if(condition){
sayHi = function(){
console.log('Hi!');
}
}else{
sayHi = function(){
console.log('Yo!');
}
}
用函数表达式写法,如预期一样,根据condition的值为变量sayHi赋予相应的函数。
递归
在编写递归函数时,arguments.callee是引用当前函数的首选。
function factorial(num){
if(num <= 1){
return 1;
}else{
return num * arguments.callee(num-1);
}
}
在严格模式下运行的代码,是不能访问arguments.callee,因为访问会出错。 此时,可以使用命名函数表达式达到目的。
const factorial = (function f(num) {
if(num <= 1){
return 1;
}else{
return num * f(num-1);
}
})
即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题,这个模式在严格模式和非严格模式下都可以使用。
尾调用优化
ES6规范新增一项内存管理优化机制,让JS引擎在满足条件时可以重用栈帧。 这项优化非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。
尾调用优化的条件
- 代码在严格模式下执行;
- 外部函数的返回值是对尾调用函数的调用;
- 尾调用函数返回后不需要执行额外的逻辑;
- 尾调用函数不是引用外部函数作用域中自由变量的闭包。
以下符合尾调用优化条件的例子:
"use strict";
// 有优化:栈帧销毁前执行参数计算
function otherFunc(a,b){
return innerFunc(a+b);
}
// 有优化:初次返回值不涉及栈帧
function otherFunc(a,b){
if(a<b){
return a;
}
return innerFunc(a+b);
}
// 有优化:两个内部函数都在尾部
function otherFunc(condition){
return condition? innerFuncA() : innerFuncB();
}
尾调用优化的代码
使用两个嵌套的函数,外部函数作为基础框架,内部函数执行递归:
"use strict"
// 基础框架,计算斐波那契数列的函数
function fib(n){
return fibImpl(0,1,n);
}
// 执行递归
function fibImpl(a,b,n){
if(n === 0){
return a;
}
return fibImpl(b, a+b, n-1);
}
代码重构之后,满足尾调用优化条件,再调用fib(1000)就不会对浏览器造成威胁了。
闭包
是指那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
// 前面举例的比较函数CompareFunc(propertyName)中
let value1= obj1[propertyName];
let value2= obj2[propertyName];
在调用一个函数时,会为这个函数调用创建一个执行上下文,并创建一个作用域链。然后用 arguments和其它命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数 作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象,直到全局 执行上下文才终止。
function compare(value1, value2){
if(value1 < value2){
return -1;
}else if(value1 > value2){
return 1;
}else{
return 0;
}
}
let result = compare(5,10);
compare()是在全局上下文中调用的,第一次调用此函数时,会为它创建一个包含arguments、value1和 value2的活动对象,是其作用域链的第一个对象。而全局上下文的变量对象则是compare()作用域链上的第二个对象,其中包含 this、result和compare。
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫做变量对象,会在代码执行期间始终存在。 而函数局部上下文中的叫做活动对象,只在函数执行期间存在。作用链其实是一个包含指针的列表,每个指针分别指向一个变量对象,但 物理上并不会包含相应的对象。
闭包的作用域链中包含自己的一个变量对象,然后是包含函数的变量对象,直到全局上下文的变量对象。
建议在使用闭包时要谨慎,仅在十分必要时使用。因为闭包会保留它们包含韩素华的作用域,所以比其他函数更占用内存。
通常,函数作用域及其中的所有变量在函数执行完毕后都会被销毁。闭包在被函数返回之后,其作用域会一直保存在内存中,直到闭包被销毁。
this对象
如果在全局函数中调用,则this在非严格模式下等于window,在严格模式等于undefined。
window.identity = 'The Window';
let obj = {
identity : 'My Obj',
getIdentity(){
let that = this;
return function(){
that.identity;
};
}
};
console.log(obj.getIdentity()()); // 'My Obj'
this和arguments都是不能直接在内部函数中访问的,如果想访问包含作用域中的arguments,则同样需要将其引用先保存 到闭包能访问的另一个变量中。
立即调用的函数表达式
立即调用的匿名函数,又称为立即调用的函数表达式。如果不在包含作用域中,将返回值赋给一个变量,则其包含的所有变量都会被销毁。
(function(){
// 块级作用域
}) ();
使用ES块级作用域变量,让每次点击<div>都显示正确的索引。
let divs = document.querySelectorAll('div');
for(let i = 0; i< divs.length;++i){
divs[i].addEventListener('click',function(){
console.log(i);
});
}
在ES6中,若对for循环使用块级作用域变量关键字,在这里就是let,则循环会为每个循环创建 独立的变量,从而让每个点击处理程序都能引用特定的索引。
私有变量
任何定义在函数或块中的变量,都可以认为是私有的。
私有变量包括函数参数、局部变量,以及函数内部定义的其它函数。
特权方法是能够访问函数私有变量(及私有函数)的公有方法。在对象上两种方式创建特权方法:
- 在构造函数中实现
function MyObj(){
let privateVariable = 30;
function privateFunc(){
return false;
}
// 特权方法,其实是一个闭包,具有访问构造函数中定义的所有变量和函数的能力
this.privateMethod = function(){
privateVariable++;
return privateFunc();
}
}
静态私有变量
- 通过私有作用域定义私有变量和函数来实现,即使用原型模式通过自定义类型中实现
(function(){
let privateVariable = 10;
function privateFunc(){
return false;
}
MyObj = function(){}; // 构造函数
MyObj.prototype.publicMethod = function(){ // 公有和特权方法,定义在原型上
privateVariable++;
return privateFunc();
}
})
不使用关键字声明的变量,会创建在全局作用域中,MyObj变成了全局变量,注意在严格模式下给未声明的变量赋值会导致错误。
使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需时间也越多。
模块模式
在一个单例对象上实现了相同的隔离和封装。单例对象就是只有一个实例的对象,JS通过对象字面量来创建单例对象的:
let singleton = {
name : value,
method(){
// 方法代码
}
};
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法。其样板代码如下:
let singleton = {
let privateVariable = 10;
function privateFunc(){
return false;
}
// 特权/公有方法和属性, 对象字面量
return{
publicProperty : true,
publicMethod(){
privateVariable++;
return privateFunc();
}
};
}();
本质上,对象字面量定义了单例对象的公共接口。如果单例对象需要进行某种初始化,并且需要访问私有变量时,可以采取这种模式:
let application = function(){
let components = new Array(); // 私有变量和私有函数
components.push(new BaseComponent()); // 初始化
// 公共接口,对象字面量
return{
getComponentCount(){
return components.length;
},
registerComponent(component){
if(typeof component == 'object'){
components.push(component);
}
}
};
}();
在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有数据,而这些数据可以通过其暴露的公共方法来访问。
以这种方式创建的每个单例对象都是Object的实例,因为最终单例都由一个对象字面量来表示。
模块增强模式
在返回对象之前先对其进行增强。适合单例对象需要是某种特定类型的实例,但又必须给它添加额外属性或方法的场景。
let singleton = function(){
let privateVariable = 10;
function privateFunc(){
return false;
}
let obj = new CustomType(); // 创建对象
obj.publicProperty = true; // 添加特权/公有属性和方法
obj.publicMethod = function(){
privateVariable++;
return privateFunc();
};
return obj; // 返回对象
}();
若前一节的application对象必须是BaseComponent的实例,则可以使用以下代码创建它:
let application = function(){
let components = new Array(); // 私有变量和私有函数
components.push(new BaseComponent()); // 初始化
let app = new BasComponent();
// 公共接口
app.getComponentCount(){
return components.length;
};
app.registerComponent(component){
if(typeof component == 'object'){
components.push(component);
}
};
return app; // 返回实例
}();
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!