函数
1.几种实例化函数对象的方式
-
以函数声明的方式定义
-
函数表达式
-
箭头函数(arrow function)
-
使用Function构造函数
接收任意多个字符串参数,最后一个参数始终会被当成函数体,而之前的参数都是新函数的参数。
不推荐使用:这段代码会被解释两次,第一次是将它当作常规ECMAScript代码,第二次是解释传给构造函数的字符串。会影响性能。
优点:可帮助理解函数作为对象,把函数名想象为指针
/* 实例化函数对象的方式 */
// 函数声明
function sum1 (num1, num2) {
return num1 + num2;
}
console.log(sum1(1,2)); // 3
// 函数表达式
let sum2 = function (num1, num2) {
return num1 + num2;
};
console.log(sum2(1,2)); // 3
// 箭头函数
let sum3 = (num1, num2) => {
return num1 + num2;
};
console.log(sum3(1,2)); // 3
// 使用Function构造函数
let sum4 = new Function("num1", "num2", "num3", "return num1 + num2 + num3");
console.log(sum4(1, 2, 5)); // 8
2.箭头函数
ES6新增。
任何可以使用函数表达式的地方,都可以使用箭头函数。
简洁的语法非常适合嵌入函数的场景:
- 如果只有一个参数,也不用圆括号;没有参数或者多个参数的情况下才需要括号
- 也可以不用大括号:不用大括号,则箭头后面就只能有一行代码,如一个赋值操作、或者一个表达式,函数会隐式返回这行代码的值
缺点:
- 不能使用arguments、super和new target
- 也不能用作构造函数
- 没有prototype属性
let multiply = (a, b) => a * b;
console.log(multiply(3, 2)); // 6
console.log(multiply.prototype); // undefined
console.log(multiply.name); // multiply
// new multiply(3, 2); // TypeError: multiply is not a constructor
3.函数名
函数名就是指向函数(对象)的指针。
使用不带括号的函数名会访问函数指针,而不会执行函数。
function sum(num1, num2) {
return num1 + num2;
}
let anotherSum = sum;
sum = null;
anotherSum(1, 2); // 3
把sum设置为null之后,就切断了它与函数之间的关联。
ES6中所有函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符、或者说是一个字符串化的变量名。
- 普通的函数声明,.name是函数声明的名称
- 匿名函数表达式和箭头函数赋值给变量后,获取变量.name,得到的是变量的名称(函数name属性被赋值为变量名称?),不赋值给变量直接获取.name,得到的是空字符串
- 具名函数表达式赋值给变量后,获取变量.name,得到的是函数表达式的名称
- 通过new Function创建的函数对象,不管获取赋值到的变量.name,还是直接获取.name(
(new Function()).name
),得到的都是anonymous。
如果函数是一个获取函数、设置函数,或者使用bind()实例化,那么标识符前面会加上一个前缀。
function foo() {}
foo.bind(null).name; // "bound foo"
let o = {
years:1,
get age() {
return this.years;
}
};
Object.getOwnPropertyDescriptor(o, "age").get.name; // "get age"
// 赋值函数表达式给变量后,获取变量的name属性
// 如果是匿名函数,输出变量名
// 如果是具名函数,输出函数名
console.log(sum2.name); // sum2
let sum22 = function sum23(num1, num2) {
return num1 + num2;
};
/*let sum24 = sum22;
console.log('24', sum24.name); // sum23
sum24 = sum2;
sum2 = null;console.log(sum24);
console.log('24', sum24.name); // sum2*/
console.log(sum22.name); // sum23
// sum23(2, 3); // ReferenceError: sum23 is not defined
console.log(sum4.name); // anonymous
console.log((() => {}).name); // ''
console.log((new Function()).name); // anonymous
4.函数参数
定义和调用的完全动态化。
4.1 ECMAScript函数的参数
特点: ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。
原因: ECMAScript函数的参数在内部表现为一个数组。
函数被调用时,总会接收一个数组,但函数并不关心这个数组中包含什么。在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments
对象,从中取得传进来的每个参数值。
arguments对象 ,是一个类数组对象(不是Array的实例,是Object的实例)。要确定传进来多少个参数,可以访问arguments.length属性。
ECMAScript函数的参数只是为了方便才写出来的,并不是必须写出来的。ECMAScript中的命名参数不会创建让之后的调用必须匹配的函数签名。(因为根本不存在验证命名参数的机制)
可以根据arguments对象的情况编写不同的处理逻辑,虽然不像真正的函数重载那么明确,但这已经足以弥补ECMAScript在这方面的缺失了。
arguments对象可以跟命名参数一起使用。
arguments对象中的值,始终会与对应的命名参数同步。修改arguments对象中元素的值,会自动同步到对应的命名参数,反之亦然。(它们在内存中还是分开的,只不过会保持同步)
arguments对象的长度是根据传入的参数个数确定的(与定义时的命名参数个数无关)。如果只传了一个参数,然后把arguments[1]设置为某个值,那么这个值并不会反映到第二个命名参数。
对于命名参数,如果调用时没有传这个参数,那么它的值就是undefined。类似于定义了变量但没有初始化。
严格模式下,给arguments[1]赋值不会影响第二个命名参数的值,对第二个命名参数重新赋值也不会影响arguments[1]的值;尝试重写arguments对象会报错(read-only)。
箭头函数中的参数:传给函数的参数不能使用arguments关键字访问,只能通过定义的命名参数访问。可以用一个普通函数把箭头函数包装起来。还可使用扩展操作符收集参数。
4.2 没有重载
ECMAScript函数没有签名,因为参数是由包含0个或多个值得数组表示的。没有函数签名,自然也就没有重载。可以通过检查参数的类型和数量,然后分别执行不同的逻辑来模拟函数重载。
4.3 默认参数值
在ECMAScript5.1及以前,实现默认参数的一种常用方式就是检测某个参数是否等于undefined,如果是则意味着没有传这个参数,就给它赋一个默认值。
ES6支持显式定义默认参数了。只要在函数定义中的参数后面用=就可以为参数赋一个默认值。
在使用默认参数时,arguments对象的值不反映参数的默认值,只反映传给函数的参数。修改命名参数也不会影响arguments对象(与ES5严格模式一样),它始终以调用函数时传入的值为准。
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值。
函数的默认参数只有在函数被调用时才会求值,而且计算默认值的函数只有在调用函数但未传相应参数时才会被调用。
默认参数作用域与暂时性死区:
默认参数会按照定义它们的顺序依次被初始化。所以后定义默认值的参数可以引用先定义的参数。
参数初始化顺序遵循”暂时性死区“规则,即前面定义的参数不能引用后面定义的。
4.4 参数扩展与收集
扩展操作符,函数定义中的参数列表,充分利用ECMAScript的弱类型及参数长度可变的特点。
扩展参数:
(调用时)
把传入的一个数组扩展为一个参数列表。对可迭代对象应用扩展操作符,并将其作为一个参数传入,可以将可迭代对象拆分,并将迭代返回的每个值单独传入。(在之前,一般通过apply()方法实现)
对arguments对象而言,它并不知道扩展操作符的存在,而是按照调用函数时传入的参数接收每一个值。
arguments对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中,也可以将扩展操作符用于命名参数,当然同时也可以使用默认参数。
收集参数:
(定义时)
可以使用扩展操作符把不同长度的独立参数组合为一个数组。(类似arguments对象的构造机制,不过收集参数的结果会得到一个Array实例)。
收集参数的前面如果还有命名参数,则只会收集其余的参数;如果没有则会得到空数组。因为收集参数的结果可变,所以只能把它作为最后一个参数。
箭头函数虽然不支持arguments对象,但支持收集参数的定义方式。
使用收集参数并不影响arguments对象,它仍然反映调用时传给函数的参数。
/* 关于参数 */
function sayHi(name, message) {
console.log( arguments );
console.log("Hello " + name + ", " + message);
}
sayHi('lily', 'welcome');
// Arguments(2) ["lily", "welcome", callee: ƒ, Symbol(Symbol.iterator): ƒ]
// 0: "lily"
// 1: "welcome"
// callee: ƒ sayHi(name, message)
// length: 2
// Symbol(Symbol.iterator): ƒ values()
// __proto__: Object
// Hello lily, welcome
function doAdd (num1, num2) {
arguments[1] = 10;
console.log( num2, arguments[0] + num2 );
}
doAdd(2, 3); // 10 12
doAdd(2); // undefined NaN
function doAdd2 (num1, num2) {
num2 = 10;
console.log( arguments[1] );
// arguments = { "0": 1, "1": 2, length: 2 };
}
doAdd2(2, 3); // 10
// 没有重载,定义两个同名函数只会覆盖
function addSomeNumber(num) {
return num + 100;
}
function addSomeNumber(num, num1) {
return num + 200;
}
let result = addSomeNumber(100);
console.log(result); // 300
let romanNumerals = ['I', 'II', 'III', 'IV', 'V', 'VI'];
let ordinary = 0;
function getNumerals() {
return romanNumerals[ordinary++];
}
function makeKing(name = 'Henry', numerals = getNumerals()) {
return `King ${name} ${numerals}`;
}
console.log(makeKing()); // King Henry I
console.log(makeKing('Louis', 'XVI')); // King Louis XVI
console.log(makeKing()); // King Henry II
function makeKing2(name = 'Henry', numerals = name) {
return `King ${name} ${numerals}`;
}
console.log(makeKing2()); // King Henry Henry
// (调用时)扩展参数
let values = [1, 2, 3, 4];
function getSum() {
// console.log( 'arguments.length', arguments.length );
let sum = 0;
for(let i = 0; i < arguments.length; ++ i) {
sum += arguments[i];
}
return sum;
}
console.log(getSum.apply(null, values)); // 10
console.log(getSum(...values)); // 10
console.log(getSum(-1, ...values)); // 9
function getProduct(a, b, c = 1) {
return a * b *c;
}
let getSum2 = (a, b, c = 0) => {
return a + b + c;
}
console.log(getProduct(...[1, 2])); // 2
console.log(getProduct(...[1, 2, 3])); // 6
console.log(getProduct(...[1, 2, 3, 4])); // 6
console.log(getSum2(...[0 ,1])); // 1
console.log(getSum2(...[0 ,1, 2])); // 3
console.log(getSum2(...[0 ,1, 2, 3])); // 3
// (定义时)收集参数
function getSum3 (...values) {
return values.reduce((x, y) => x + y, 0);
}
console.log( getSum3(1, 2, 3) ); // 6
function ignoreFirst(firstValue, ...values) {
// console.log(arguments.length);
console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1, 2); // [ 2 ]
ignoreFirst(1, 2, 3); // [ 2, 3 ]
let getSum4 = (...values) => values.reduce((x, y) => x + y, 0);
console.log( getSum4(1, 2, 3) ); // 6
5.函数声明与函数表达式(比对:提升)
函数声明提升(function declaring hoisting):JavaScript引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。(JavaScript引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部)
而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
6. 函数作为值(引用值)
函数名在ECMAScript中就是变量,所以函数可以用在任何可以使用变量的地方。(其他函数的参数,其他函数的返回值)
可以把函数作为参数传给另一个函数,也可以在一个函数中返回另一个函数。
如果是访问函数而不是调用函数,就必须不带括号。
其他:默认情况下,数组的sort()方法要对数组元素执行toString(),然后再决定它们的顺序。
/* 函数作为引用值 */
function callSomeFunction(someFunc, someArg) {
return someFunc(someArg);
}
function add10(num) {
return num + 10;
}
let result1 = callSomeFunction(add10, 10);
console.log(result1); // 20
function createComparisonFunction(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: "Zachary", age: 28},
{name: "Nicholas", age: 29}
];
data.sort(createComparisonFunction("name"));
console.log(data[0].name); // Nicholas
data.sort(createComparisonFunction("age"));
console.log(data[0].name); // Zachary
7. 函数内部对象
ES5中,函数内部存在两个特殊的对象:arguments和this。ES6又新增了new.target属性。
-
arguments
一个类数组对象,包含调用函数时传入的所有参数。
arguments对象还有一个callee属性,是一个指向arguments对象所在函数的指针。使用arguments.callee就可以让函数逻辑与函数名解耦。(递归)
-
this
在标准函数和箭头函数中有不同的行为。
在标准函数中,与调用方式有关,必须到函数被调用时才能确定。在代码执行的过程中可能会变。
在箭头函数中,this引用的是定义箭头函数的上下文;this会保留定义该函数时的上下文。
-
caller
ES5给函数对象上添加的一个属性。
这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。
如果要降低耦合度,可以通过
arguments.callee.caller
来引用同样的值。严格模式下,访问caller和callee都会报错。让第三方代码无法检测同一上下文中运行的其他代码。
-
new.target
检测函数是否使用
new
关键字调用如果不是,则为undefined;如果是,则new.target将引用被调用的构造函数。
// 函数内部的特殊对象
function factorial(num) {
if(num <= 1) return 1;
else {
return num * arguments.callee(num - 1);
}
}
let trueFactorial = factorial;
factorial = function () {
return 0;
}
console.log(trueFactorial(5)); // 120
console.log(factorial(5)); // 0
/*
在浏览器中运行
window.color = 'red';
let o = {
color: 'blue'
};
let sayColor = () => console.log(this.color);
sayColor(); // red
o.sayColor = sayColor;
o.sayColor(); // red
*/
function outer() {
inner();
}
function inner() {
// "use strict";
// TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
console.log(inner.caller);
console.log(arguments.callee.caller);
console.log(arguments.caller); // undefined
}
outer();
// [Function: outer]
// [Function: outer]
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(); // throw 'King must be instantiated using "new"';
8. 函数属性与方法
-
属性
-
length:保存函数定义的命名参数的个数
-
prototype:保存引用类型所有实例方法的地方,进而由所有实例共享。(在自定义类型时特别重要)
Object.getOwnPropertyDescriptor(foo, 'name'); // {value: "foo", writable: false, enumerable: false, configurable: true} Object.getOwnPropertyDescriptor(foo, 'prototype'); // {value: {…}, writable: true, enumerable: false, configurable: false}
-
-
方法
-
apply、call
以指定的this值来调用函数。
apply接收两个参数:函数内的this值和一个参数数组(Array实例或arguments对象)
call方法与apply的作用一样,只是传参的形式不同。第一个参数也是参数内this的值,剩下的是要传给被调用函数的参数列表。通过call向函数传参时,必须将参数一个一个地列出来。
要使用哪个,完全取决于怎么给要调用的函数传参更方便。如果不用给被调用的函数传参,则使用哪个方法都一样。
好处是,可以将任意对象设置为任意函数的作用域,这样对象就可以不用关心方法。
-
bind
bind方法会创建一个新的函数实例,其this值会被绑定到传给
bind()
的对象。 -
继承的方法
toLocaleString()和toString()始终返回函数的代码。具体格式因浏览器而异。
valueOf()返回函数本身(无法new操作,也没有prototype)
-
function sum5(num1, num2) {
return num1 + num2;
}
function callSum1(num1, num2) {
return sum5.apply(this, arguments);
}
function callSum2(num1, num2) {
return sum5.apply(this, [num1, num2]);
}
function callSum(num1, num2) {
// return sum5.call(this, num1, num2);
return sum5.call(this, ...arguments);
}
console.log(callSum1(10, 10)); // 20
console.log(callSum2(10, 10)); // 20
console.log(callSum(10, 10)); // 20
/*
在浏览器中运行
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
*/
9. 函数表达式
函数表达式最常见的形式:(创建函数并赋值给变量)
let functionName = function(arg0, arg1, arg2) {
// body...
};
这样创建的函数叫做匿名函数(anonymous function),因为function关键字后面没有标识符。(有时也被称为兰姆达函数)。
未赋值给其他变量的匿名函数的name属性是空字符串。
不建议以下的使用形式:
if(condition) {
function sayHi() {
// ...
}
} else {
function sayHi() {
// ...
}
}
JavaScript引擎会尝试将其纠正为适当的声明。各浏览器纠正这个问题的方式并不一致。(兼容)
把以上的函数声明换成函数表达式就没问题了。
任何时候,只要函数被当作值来使用,它就是一个函数表达式。(6)
10. 递归
通常的形式是一个函数通过名称调用自己。
但如果把这个函数赋值给其他变量,就可能会出问题。在写递归函数时使用arguments.callee
可以避免这个问题(严格模式下不能用callee)。
arguments.callee
就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用。是引用当前函数的首选。
严格模式下,可以使用命名函数表达式(named function expression)达到目的。如:
const factorial = (function f(num) {
if(num < 1) {
return 1;
} else {
return num * f(num - 1);
}
});
即使把函数赋值给另一个变量,函数表达式的名称f也不变,因此递归调用不会有问题。
let factorial2 = (function f(num) {
if(num <= 1) return 1;
else return num * f(num - 1);
});
console.log(factorial2(2)); // 2
let anotherFactorial = factorial2;
factorial2 = null;
console.log(anotherFactorial(3)); // 6
let obj = {
num: 3,
factorial: (function f(num) {
if(num <= 1) return 1;
else return num * f(num-1);
// else return num * this.factorial(num-1); 两个写法都可以
})
};
console.log(obj.factorial(3)); // 6
11. 尾调用优化
ES6新增了一项内存管理优化机制,让JavaScript引擎在满足条件时可以重用栈帧。非常适合“尾调用”,即外部函数的返回值是一个内部函数的返回值。
如:
function outerFunction() {
return innerFunction(); // 尾调用
}
ES6优化之前,执行这个例子会在内存中发生如下操作:
1)执行到outerFunction函数体,第一个栈帧被推到栈上;
2)执行outerFunction函数体,到return语句。计算返回值必须先计算innerFunction;
3)执行到innerFunction函数体,第二个栈帧被推到栈上;
4)执行innerFunction函数体,计算其返回值;
5)将返回值传回outerFunction,然后outerFunction再返回值;
6)将栈帧弹出栈外。
ES6优化之后,发生如下操作:
1)执行到outerFunction函数体,第一个栈帧被推到栈上;
2)执行outerFunction函数体,到达return语句。为求值返回语句,必须先求值innerFunction;
3)引擎发现把第一个栈帧弹出栈外也没问题,因为innerFunction的返回值也是outerFunction的返回值;
4)弹出outerFunction的栈帧;
5)执行到innerFunction函数体,栈帧被推到栈上;
6)执行innerFunction函数体,计算其返回值;
7)将innerFunction的栈帧弹出栈外。
第一种情况下每多调用一次嵌套函数,就会多增加一个栈帧。第二种情况下,无论调用多少次嵌套函数,都只有一个栈帧。
ES6尾调用优化的关键:如果函数的逻辑允许基于尾调用将其销毁,则引擎就会那么做。
11.1 尾调用优化的条件
确定外部栈帧真的没有必要存在了。
涉及的条件如下:
- 代码在严格模式下执行;
- 外部函数的返回值是对尾调用函数的调用;
- 尾调用函数返回后不需要执行额外的逻辑;
- 尾调用函数不是引用外部函数作用域中自由变量的闭包。
差异化尾调用和递归尾调用。引擎并不区分尾调用中调用的是函数自身还是其他函数。但这个优化在递归场景下的效果最明显,因为递归代码最容易在栈内存中迅速产生大量栈帧。
非严格模式下,函数调用中允许使用f.arguments和f.caller,它们都会引用外部函数的栈帧。——意味着不能应用优化了。
11.2 尾调用优化的代码(例子)
斐波那契数列。
把简单的递归函数转换为待优化的代码(可优化的代码)。把递归改写成迭代循环形式。
12. 闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
这个作用域链一直向外串起了所有包含函数的活动对象,直到全局执行上下文才终止。
在函数执行时,要从作用域链中查找变量,以便读、写值。
函数执行时,每个执行上下文中都会有一个包含其中变量的对象。全局上下文中的叫变量对象,它会在代码执行期间始终存在;而函数局部上下文中的叫活动对象,只在函数执行期间存在。
标签:Function,10,console,函数,function,JavaScript,参数,arguments,log From: https://www.cnblogs.com/halftonine/p/16789639.html