重学JavaScript Day6

100 阅读3分钟

Ch10 函数

声明函数的三种方法:

function sum(num1, num2) {
    return num1 + num2;
}
​
let sum = function (num1, num2) {
    return num1 + num2;
}
​
let sum = new Function("num1", "num2", "return num1 + num2"); //不推荐

10.1 箭头函数

箭头函数(=>)是ES6新特性。比较适合嵌入函数或者回调函数的书写

使用:

let ints = [1,2,3]
ints.map(function(i) { return i + 1; })
// 等价于
ints.map((i) => i + 1)

当函数体内只有一行代码,比如赋值操作或者一个表达式,就可以不用大括号,并且省略大括号的写法会返回这行代码的执行结果

10.2 函数名

ES6的所有函数对象都会暴露一个只读属性name,其中包括了关于函数的信息。

function foo(){}
let bar = function(){};
let baz = ()=>{};
​
foo.name;       // foo
bar.name;       // bar
baz.name;       // baz
(()=>{}).name   // (空字符串)
(new Function()).name   // anonymous

10.3 函数参数

在js中,函数既不关心传入的参数个数,也不 关心这些参数的数据类型。定义函数时要接收两个参数,并不意味着调用时就传两个参数。你可以传一 个、三个,甚至一个也不传,解释器都不会报错。

在使用function关键字(非箭头)定义函数时,可以在函数体内通过arguments对象获得传进来的参数。arguments是一个类数组对象,并不是数组的实例,因此在对arguments对象使用数组方法时,需要通过call或者apply来实现。如:Array.prototype.slice.call(arguments, 0)

修改arguments中的值会同时影响到函数传进来的参数。但是在严格模式下,即使修改arguments的值,函数参数的值仍然保持不变,而且在函数中尝试重写arguments会报错。

function doAdd(num1, num2) {
    arguments[1] = 100;
    return num1 + num2;
}
doAdd(1,20);    // 101

10.4 没有重载

JavaScript不像其他语言,一个函数可以有两个定义。在Js中,函数没有签名(接收参数的类型和数量),因此没有重载。如果在JS中定义了两个名字相同的函数,那么后定义的会覆盖先定义的

10.5 默认参数

ECMAScript 6 支持显式定义默认参数了。

function makeKing(name = 'Henry') { 
 return `King ${name} VIII`; 
} 
console.log(makeKing('Louis')); // 'King Louis VIII' 
console.log(makeKing()); // 'King Henry VIII'

10.6 参数扩展与收集

扩展参数

function getSum() { 
 let sum = 0; 
 for (let i = 0; i < arguments.length; ++i) { 
 sum += arguments[i]; 
 } 
 return sum; 
} 
​
let values = [1, 2, 3, 4];
console.log(getSum(...values));     // 10

收集参数

在构思函数定义时,可以使用扩展操作符把不同长度的独立参数组合为一个数组。

function getSum(...values) { 
 // 顺序累加 values 中的所有值
 // 初始值的总和为 0 
 return values.reduce((x, y) => x + y, 0); 
} 
console.log(getSum(1,2,3)); // 6 

10.7 函数声明与函数表达式

由于函数提升的存在,JavaScript引擎会在执行代码前读取所有函数声明,并生成函数定义。而函数表达式必须等到代码执行到它那一行,才会在上下文中声明函数定义。

// 函数声明提升
console.log(sum(10, 10)); 
function sum(num1, num2) { 
 return num1 + num2; 
} 
​
// 会出错
console.log(sum(10, 10)); 
let sum = function(num1, num2) { 
 return num1 + num2; 
}; 
​

10.9 函数内部

在函数内部存在三个特殊对象:arguments、this以及 new.target 。

10.9.1 arguments

arguments 对象是一个类数组对象,包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数时才会有。arguments 对象还有一个 callee 属性,是一个指向 arguments 对象所在函数的指针。callee存在的意义是在递归的时候,让函数逻辑与函数名解耦。

// 求阶乘函数
function factorial(num) { 
    if (num <= 1) { 
        return 1; 
    } else { 
        return num * factorial(num - 1); 
        // 上面这一行等价于下面一行
        return num * arguments.callee(num - 1);
    } 
} 

10.9.2 this

在非箭头函数中,this 引用的是调用该函数的上下文对象(在 网页的全局上下文中调用函数时,this 指向 windows)。

在箭头函数中,this引用的是定义箭头函数的上下文

这里简单来说归为两点:

  • 普通函数的this指向在执行时决定
  • 箭头函数的this指向在定义时决定

10.9.3 caller

ES5 给函数对象上添加了一个属性: caller,指向调用当前函数的函数。

function outer() { 
 inner(); 
} 
function inner() { 
 console.log(inner.caller); 
} 
outer();            // f outer(){...}

10.9.4 new.target

ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。

  • 如果函数是正常调用的,则 new.target 的值是 undefined;
  • 如果是使用 new 关键字调用的,则 new.target 将引用被调用的构造函数。

10.10 函数属性和方法

JavaScript中的函数是对象,因此会有属性和方法。

每个函数有两个属性:length和prototype。其中length用来表示函数的参数个数; prototype 是保存引用类型所有实例方法的地方。

每个函数还有三个方法:apply,call,bind。这三个方法的作用是以指定的this值来执行函数,简单来说就是改变this指向。

  • apply接收两个参数,第一个是this指向,另一个是参数数组(可以是Array实例,也可以是arguments)。
  • call作用与apply一样,第一个参数是this指向,后面的参数会逐个传进被调用函数的参数中。
  • bind传参与call相同,与call不同的是,它返回另外一个函数实例。
function sum(num1, num2) { 
    return num1 + num2; 
} 
function callSum1(num1, num2) { 
    return sum.apply(this, arguments);
}
function callSum(num1, num2) { 
    return sum.call(this, num1, num2); 
}
function callSum3(num1, num2) { 
    return sum.bind(this, num1, num2); 
}
// 调用callSum3要这么调用
callSum3(1,2)()     // 3

10.11 函数表达式

const functionName = function(){}

函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量 functionName。这样创建的函数叫作匿名函数(anonymous funtion),因为 function 关键字后面没有标识符。(匿名函数有也时候也被称为兰姆达函数)。

函数表达式的优点:函数可以根据情况而定,灵活多变。

// 千万别这样做!
if (condition) { 
    function sayHi() { 
        console.log('Hi!'); 
    } 
} else { 
    function sayHi() { 
        console.log('Yo!'); 
    } 
} 
​
// 正确做法
let sayHi;
if (condition) { 
    sayHi = function sayHi() { 
        console.log('Hi!'); 
    } 
} else { 
    sayHi = function sayHi() { 
        console.log('Yo!'); 
    } 
}

10.14 闭包

闭包指的是引用了另外一个函数作用域中变量的函数,通常实在嵌套函数中实现。

function makeFunc() {
    var name = "Mozilla";
    
    return function () {
        alert(name);        // 闭包
    }       
}
​
var myFunc = makeFunc();
myFunc();

10.14.1 this

在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义,则 this 对象会在运行时绑定到执行函数的上下文。匿名函数在这种情况下不会绑定到某个对象,这就意味着 this 会指向 window,

window.identity = 'The Window'; 
let object = { 
 identity: 'My Object', 
 getIdentityFunc() { 
    return function() { 
        return this.identity; 
    }; 
 } 
}; 
console.log(object.getIdentityFunc()()); // 'The Window'

那么我们如何访问到外部函数的this呢?两种方法,一种使用箭头函数,另外一种用变量保存this。

let object = { 
 identity: 'My Object', 
 getIdentityFunc() { 
    return ()=> { 
        return this.identity; 
    }; 
 } 
};
console.log(object.getIdentityFunc()()); // 'My Object'let object = { 
 identity: 'My Object', 
 getIdentityFunc() {
    let that = this;
    return ()=> { 
        return that.identity; 
    }; 
 } 
};
console.log(object.getIdentityFunc()()); // 'My Object'

10.15 立即调用函数表达式IIFE

简单的例子:用IIFE来模拟块级作用域。

// IIFE 
(function () { 
    for (var i = 0; i < count; i++) { 
        console.log(i); 
    } 
})(); 
console.log(i); // 抛出错误