函数
# 函数名
函数名就是指向函数的指针
function sum(num1,num2){
return num1 + num2;
}
log(sum(10,10)) // 20
let anotherSum = sum;
log(anotherSum(10,10)) // 20
sum = null;
log(anotherSum(10,10)) // 20
anotherSum
和sum
都指向同一个函数。调用anotherSum()
也可以返回结果。把sum
设置为null
之后,就切断了它与函数之间的关联。而anotherSum()
还是可以照常调用,没有问题。
ECMAScript 6
的所有函数对象都会暴露一个只读的name属性,其中包含关于函数的信息。多数情况下,这个属性中保存的就是一个函数标识符,或者说是一个字符串化的变量名。即使函数没有名称,也会如实显示成空字符串。如果它是使用Function构造函数创建的,则会标识成"anonymous"
function foo(){}
let bar = function (){}
let baz = ()=>{};
log(foo.name); // foo
log(bar.name); // bar
log(baz.name); // baz
log((()=>{}).name) // (空字符串)
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 propertyDescriptor = Object.getOwnPropertyDescriptor(dog,"age"); // bound foo
log(propertyDescriptor.get.name) // get age
log(propertyDescriptor.set.name) // set age
# 参数
ECMAScript
函数既不关心传入的参数个数,也不关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一个、三个,甚至一个也不传,解释器都不会报错。
function sayHi_one(name,message){
log("hello " + name + ", " + message)
}
function sayHi_two(){
log("hello " + arguments[0] + ", " + arguments[1])
}
sayHi_one("张三","Z") // hello 张三, Z
sayHi_two("张三","Z") // hello 张三, Z
传入参数的个数
function howManyArgs(){
log(arguments.length);
}
howManyArgs("string",45); // 2
howManyArgs(); // 0
howManyArgs(12); // 1
多参数传递
function doAdd(){
if(arguments.length === 1){
log(arguments[0] + 10)
}else if(arguments.length === 2){
log(arguments[0] + arguments[1])
}
}
doAdd(10); // 20
doAdd(30,20) // 50
箭头函数没有arguments对象
let bar = ()=>{
log(arguments);
}
bar(5); // Uncaught ReferenceError: arguments is not defined
虽然箭头函数中没有arguments对象,但可以在包装函数中把它提供给箭头函数
function foo(){
let bar = ()=>{
log(arguments[0]); // 5
}
bar()
}
foo(5);
# 没有重载
定义了两个同名函数,则后定义的会覆盖先定义的
function addSomeNumber(num){
return num + 100;
}
function addSomeNumber(num){
return num + 200;
}
let result = addSomeNumber(100);
log(result) // 300
# 默认参数
function makeKing(name = "Henry"){
return `King ${name} VIII`;
}
log(makeKing("Louis")); // King Louis VIII
log(makeKing()); // King Henry VIII
给参数传undefined相当于没有传值,不过这样可以利用多个独立的默认值
function makeKing(name = "Henry", numerals = 'VIII'){
return `King ${name} ${numerals}`;
}
log(makeKing()); // King Henry VIII
log(makeKing('Louis')); // King Louis VIII
log(makeKing(undefined,'VI')); // King Henry VI
修改命名参数也不会影响arguments对象,它始终以调用函数时传入的值为准
function makeKing(name = 'Henry'){
name = 'Louis';
return `King ${arguments[0]}`;
}
console.log(makeKing()); // King undefined'
console.log(makeKing('Louis2')); // King Louis
默认参数值并不限于原始值或对象类型,也可以使用调用函数返回的值
let romanNumerals = [`I`,'II','III','IV','V','VI'];
let ordinality = 0;
function getNumerals(){
return romanNumerals[ordinality++];
}
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
console.log(makeKing()); // King Henry III
给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样
function makeKing(name = 'Henry', numerals = 'VIII') {
return `King ${name} ${numerals}`;
}
// 想象一下这个过程
function makeKing() {
let name = 'Henry';
let numerals = 'VIII';
return `King ${name} ${numerals}`;
}
因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数
function makeKing(name = 'Henry',numerals=name){
return `King ${name} ${numerals}`
}
console.log(makeKing()); // King Henry Henry
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。像这样就会抛出错误
// 调用时不传第一个参数会报错
function makeKing(name = numerals,numerals="VIII"){
return `King ${name} ${numerals}`
}
console.log(makeKing()); // 报错
console.log(makeKing('Louis')); // King Louis VIII
参数也存在于自己的作用域中,它们不能引用函数体的作用域
// 调用时不传第二个参数会报错
function makeKing(name = 'Henry', numerals=defaultNumeral) {
let defaultNumeral = 'VIII';
return `King ${name} ${numerals}`;
}
# 参数扩展与收集
传入的参数累加
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.apply(null,values)); // 10
// 方法二
console.log(getSum(...values)) // 10
// 传入其他值
console.log(getSum(-1,...values)); // 9
console.log(getSum(...values,5)); // 15
console.log(getSum(-1,...values,5)); // 14
console.log(getSum(...values,...[5,6,7])); // 28
收集参数
function getSum(...values){
// 顺序累加values中的所有值
// 初始值的总和为0
return values.reduce((x,y)=>x+y,0)
}
console.log(getSum(1,2,3)); // 6
因为收集参数的结果可变,所以只能把它作为最后一个参数
// 不可以
function getProduct(...values, lastValue) {}
// 可以
function ignoreFirst(firstValue, ...values) {
console.log(values);
}
ignoreFirst(); // []
ignoreFirst(1); // []
ignoreFirst(1,2); // [2]
ignoreFirst(1,2,3); // [2, 3]
使用收集参数并不影响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引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
正确
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;
}
并不是因为使用let而导致的,使用var关键字也会碰到同样的问题
console.log(sum(10,10)) // 报错
var sum = function(num1,num2){
return num1 + num2;
}
# 函数作为值
函数作为参数传递
function callSomeFunction(someFunction,someArgument){
return someFunction(someArgument);
}
function add10(num){
return num + 10;
}
let result1 = callSomeFunction(add10,10);
console.log(result1) // 20
function getGreeting(name){
return `Hello, ${name}`
}
let result2 = callSomeFunction(getGreeting,"Nicholas");
console.log(result2); // Hello, Nicholas
排序
// 分别按name和age排序
function createComparisonFunction(propertyName){
return function (object1,object2){
let value1 = object1[propertyName];
let value2 = object2[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
# 函数内部
arguments中的callee属性
错误
// 求阶乘
function factorial(num){
if(num <= 1){
return 1;
}else {
return num * factorial(num-1);
}
}
let trueFactorial = factorial;
factorial = null;
console.log(trueFactorial(5)) // 出错
正确
// 求阶乘
function factorial(num){
if(num <= 1){
return 1;
}else {
return num * arguments.callee(num-1);
}
}
let trueFactorial = factorial;
factorial = null;
console.log(trueFactorial(5)) // 120
trueFactorial
变量被赋值为factorial,实际上把同一个函数的指针又保存到了另一个位置。然后,factorial函数又被重写为一个返回0的函数。如果像factorial()最初的版本那样不使用arguments.callee
就会报错
this
this引用的是把函数当成方法调用的上下文对象
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
new.target
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(); // Uncaught King must be instantiated using "new"
# 函数属性与方法
length
属性保存函数定义的命名参数的个数
function sayName(name){
console.log(name);
}
function sum(num1,num2){
return num1 + num2;
}
function sayHi(){
console.log("hi")
}
console.log(sayName.length); // 1
console.log(sum.length); // 2
console.log(sayHi.length); // 0
prototype属性也许是ECMAScript
核心中最有趣的部分。prototype是保存引用类型所有实例方法的地方,在ECMAScript 5
中,prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性。
apply
function sum(num1,num2){
return num1 + num2;
}
function callSum1(num1,num2){
return sum.apply(this,arguments); // 传入arguments 对象
}
function callSum2(num1,num2){
return sum.apply(this,[num1,num2]); // 传入数组
}
console.log(callSum1(10,10)); // 20
console.log(callSum2(10,10)); // 20
通过call()向函数传参时,必须将参数一个一个地列出来
function sum(num1,num2){
return num1 + num2;
}
function callSum(num1,num2){
return sum.call(this,num1,num2);
}
console.log(callSum(10,10)); // 20
apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文即函数体内this值的能力。
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
bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象。
window.color = 'red';
let o = {
color:"blue"
};
function sayColor(){
console.log(this.color);
}
let objectSayColor = sayColor.bind(o);
objectSayColor(); // blue
在sayColor()
上调用bind()并传入对象o创建了一个新函数objectSayColor()。objectSayColor()
中的this值被设置为o,因此直接调用这个函数,即使是在全局作用域中调用,也会返回字符串"blue"。
# 递归
arguments.callee
就是一个指向正在执行的函数的指针,因此可以在函数内部递归调用
function factorial(num) {
if (num <= 1) {
return 1;
} else {
return num*arguments.callee(num-1);
}
}
在严格模式下运行的代码是不能访问arguments.callee
的,因为访问会出错。此时,可以使用命名函数表达式达到目的
let factorial = (function f(num){
if(num <= 1){
return 1;
}else {
return num * f(num - 1)
}
})
const b = factorial;
factorial = null;
console.log(b(5)); // 120
# 尾调用优化
# 闭包
匿名函数经常被人误认为是闭包(closure)。闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的。
# 立即调用的函数表达式
立即调用的匿名函数又被称作立即调用的函数表达式(IIFE, Immediately Invoked Function Expression)。它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。
(function (){
for(var i = 0;i<4;i++){
console.log(i)
}
})();
console.log(i) // 抛出错误
块级作用域1
{
let i;
for(i = 0;i<4;i++){
console.log(i)
}
}
console.log(i) // 抛出错误
块级作用域2
for(let i = 0;i<4;i++){
console.log(i)
}
console.log(i) // 抛出错误