函数实际上是对象,每个函数都是Function类型的实例
定义函数的方法有
-
函数表达式
let sum = function(num1, num2) { return num1 + num2; } -
函数声明
function sum(num1, num2) { return num1 + num2; } -
箭头函数
let sum = (num1, num2) => { return num1 + num2; } -
Function构造函数 (不推荐)
let sum = new Function('num1', 'num2', 'return num1 + num2')
函数名
因为函数是对象,所以函数名就是指向函数对象的指针,这意味着一个函数可以有多个名称,而且不一定与函数本身紧密绑定。
使用不带括号的函数名会访问函数指针,而不会执行函数
如果函数是一个获取函数、设置函数,或者使用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 d = Object.getOwnPropertyDescriptor(dog, 'age');
console.log(d.get.name); // get age
console.log(d.set.name); // set age
参数
ECMAScript函数既不关心传入的参数个数,也不关心这些参数的数据类型。
ECMAScript函数的参数只是为了方便才写出来的,也不存在验证命名参数的机制。
之所以会这样,主要是因为ECMAScript函数的参数在内部表现为一个数组。在使用function关键字定义(非箭头)函数时,可以在函数内部访问arguments(类数组)对象,从中取得传进来的每个参数值
arguments的值始终会与对应的命名参数同步,但这并不意味着它们都访问同一个内存地址,它们在内存中还是分开的,只不过会保持同步而已另外,
arguments对象的长度是根据传入的参数个数,而非定义函数时给出的命名参数个数
对于命名参数而言,如果调用函数时没有传入这个参数,那么它的值是undefined。这类似于定义了变量而没有初始化
严格模式下,修改
arguments的值不会再影响命名参数的值,其次,在函数中尝试重写arguments对象会导致语法错误。(代码也不会执行)
如果函数是使用箭头语法定义的,那么传给函数的参数将不能使用arguments关键字访问,而只能通过定义的命名参数访问
ECMAScript中的所有参数都是按值传递的。不可能按引用传递参数。如果把对象作为参数传递,那么传递的值就是这个对象的引用
函数的默认参数只有在函数被调用时才会求值,不会在函数定义时求值。而且,计算默认值的函数只有在调用函数但未传相应参数时才会被调用
ECMAScript函数没有签名,因为参数是由包含零个或多个值的数组表示的。没有函数签名,自然也就没有重载
把函数名当成指针有助于理解为什么ECMAScript没有函数重载
如果在ECMAScript中定义了两个同名函数,则后定义的会覆盖先定义的。
默认参数作用域与暂时性死区
因为在求值默认参数时可以定义对象,也可以动态调用函数,所以函数参数肯定是在某个作用域中求值的
给多个参数定义默认值实际上跟使用let关键字顺序声明变量一样
因为参数是按顺序初始化的,所以后定义默认值的参数可以引用先定义的参数
参数初始化顺序遵循“暂时性死区”规则,即前面定义的参数不能引用后面定义的。否则会抛出错误
// 错误示范
function k(name = nume, nume = 'a') {...}
参数也存在于自己的作用域中,它们不能引用函数体的作用域
// 调用时不传第二个参数会报错
function k(name = 'echo', nume = defaultNume) {
let defaultNume = 'a';
retrn `k ${name} ${nume}`
}
函数内部
在ES5中,函数内部存在两个特殊的对象:arguments和this
ES6又新增了new.target属性
arguments
它是一个类数组对象,包含调用函数时传入的所有参数。
这个对象只有以function关键字定义函数(相对于使用箭头语法创建函数)时才会有
虽然主要用于包含函数参数,但arguments对象其实还有一个callee属性,是一个指向arguments对象所在函数的指针
使用arguments.callee可以让函数逻辑与函数名解耦
function factorial(num) {
if(num <= 1) {
return 1;
} else {
// 使用arguments.callee代替了函数名,这意味着无论函数叫什么名称,都可以引用正确的函数
return num * arguments.callee(num - 1);
}
}
this
this在标准函数和箭头函数中有不同的行为
在标准函数中,this引用的是把函数当成方法调用的上下文对象,这时候通常称其为this值(在网页的全局上下文中调用函数时,this指向windows)
window.color = 'red';
let o = {
color: 'blue'
};
function sayColor() {
console.log(this.color);
}
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'blue'
在箭头函数中,this引用的是定义箭头函数的上下文。
window.color = 'red';
let o = {
color: 'blue'
}
let sayColor = () => color.log(this.color);
sayColor(); // 'red'
o.sayColor = sayColor;
o.sayColor(); // 'red'
在对sayColor()的两次调用中,this引用的都是window对象,因为这个箭头函数是在window上下文中定义的
在事件回调或定时回调中调用某个函数时,this值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的this会保留定义该函数时的上下文
function king() {
this.royaltyName = 'echo';
// this引用king的实例
setTimeout(() => console.log(this.royaltyName), 1000);
}
this对象
在闭包中使用this会让代码变复杂。
如果内部函数没有使用箭头函数定义,则this对象会在运行时绑定到执行函数的上下文。
如果在全局函数中调用,则this在非严格模式下等于window,在严格模式下等于undefined
如果作为某个对象的方法调用,则this等于这个对象
匿名函数在这种情况下不会绑定到某个对象,这就意味着this在非严格模式下会指向window
每个函数在被调用时都会自动创建两个特殊变量:this和arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是,如果把this保存到闭包可以访问的另一个变量中,则是行得通的
window.identity = 'The window';
let obj = {
identity: 'obj',
getIdentityFn() {
let that = this;
return function() {
return that.identity;
}
}
}
console.log(obj.getIdentityFn()()); // 'obj'
caller
ES5也会给函数对象上添加一个属性:caller。这个属性引用的是调用当前函数的函数,或者如果是在全局作用域中调用的则为null。
function outer() {
inner();
}
function inner() {
console.log(inner.caller);
}
outer();
以上代码ourter()调用了inner(),inner.caller指向outer()。
如果要降低耦合度,则可以通过arguments.callee.caller来引用同样的值:
...
console.log(arguments.callee.caller);
...
在严格模式下访问arguments.callee会报错。
ES5也定义了arguments.caller,但在严格模式下访问它会报错,在非严格模式下则始终是undefined。
这是为了分清
arguments.caller和函数的caller而故意为之的。
严格模式下还有一个限制,就是不能给函数的caller属性赋值,否则会导致错误
new.target
ES6新增了检测函数是否使用new关键字调用的new.target属性。如果函数是正常调用的,则new.target的值是undefined;如果是使用new关键字调用的,则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(); // Error: King must be instantiated using "new"
函数属性与方法
在JS中,函数也是对象,因此有属性和方法。每个函数都有两个属性
-
length保存函数定义的命名参数的个数 -
prototype保存引用类型所有实例方法的地方这意味着
toString()、valueOf()等方法实际上都保存在prototype上,进而由所有实例共享。在ES5中,
prototype属性是不可枚举的,因此使用for-in循环不会返回这个属性
函数还有两个方法:
-
apply(this, [params1, params2, ...]) -
call(this, params1, params2, ...)
在严格模式下,调用函数时如果没有指定上下文对象,则this值不会指向window。除非使用apply()或call()把函数指定给一个对象,否则this的值会变成undefined
apply()和call()真正强大的地方并不是给函数传参,而是控制函数调用上下文,即函数体内this值的能力
ES5出于同样的目的定义了一个新方法:bind()
bind()方法会创建一个新的函数实例,其this值会被绑定到传给bind()的对象:fn.bind(obj)
私有变量
严格来讲,JS没有私有成员的概念,所有对象都公有的。不过,倒是有私有变量的概念
任何定义在函数或块中的变量,都可以认为是私有的,因为在这个函数或块的外部无法访问其中的变量
私有变量包括函数参数、局部变量,以及函数内部定义的其他函数
如果函数中创建了一个闭包,则这个闭包能通过其作用域链访问其外部的私有变量。基于这一点,就可以创建出能够访问私有变量的公有方法
特权方法是能够访问函数私有变量(及私有函数)的公有方法。在对象上有两种方式创建特权方法:
-
在构造函数中实现
function MyObj() { // 私有变量和私有函数 let p = 10; function pFun() { return false; } // 特权方法 this.pubM = function() { p++; return pFun(); } }这个模式是把所有私有变量和私有函数都定义在构造函数中。然后,再创建一个能够访问这些私有成员的特权方法
这样做之所以可行,是因为定义在构造函数中的特权方法其实是一个闭包,它具有访问构造函数中定义的所有变量和函数的能力
-
使用静态私有变量实现特权方法
通过使用私有作用域定义私有变量和函数来实现
(function() { // 私有变量和私有函数 let p = 10; function pFun() { return false; } // 构造函数 M1yObj = function() {}; // 公有和特权方法 MyObj.prototype.pubM = function() { p++; return pFun(); } })();在这个模式中,匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。
在这里声明MyObj并没有使用任何关键字,所以它变成了全局变量,可以在这个私有作用域外部被访问。
注意在严格模式下给未声明的变量赋值会导致错误
这个模式与前一个模式的主要区别就是,私有变量和私有函数是由实例共享的。因为特权方法定义在原型上,所以同样是由实例共享的。特权方法作为一个闭包,始终引用着包含它的作用域。
使用闭包和私有变量会导致作用域链变长,作用域链越长,则查找变量所需的时间也越多。
箭头函数
箭头函数简洁的语法非常适合嵌入函数的场景
[1, 2, 3].map(i => i + 1);
箭头函数使用{}说明包含“函数体”,可以跟常规的函数一样包含多条语句。如果不使用{},那么箭头后面就只能有一行代码,且会隐式返回这行代码的值
箭头函数虽然语法简洁,但也有很多场合不适用
-
箭头函数不能使用
arguments、super和new.target箭头函数虽然不支持
arguments对象,但支持收集参数的定义方式,因此也可以实现与使用arguments一样的逻辑let getSum = (...values) => { return values.reduce((x, y) => x + y, 0); } console.log(getSum(1, 2, 3)); // 6 -
箭头函数不能用作构造函数
-
箭头函数没有
prototype属性
函数声明与函数表达式
事实上,JS引擎在加载数据时对它们是区别对待的。
JS引擎在任何代码执行之前,会先读取函数声明,并在执行上下文中生成函数定义;而函数表达式必须等到代码执行到它那一行,才会在执行上下文中生成函数定义。
除了函数什么时候真正有定义这个区别之外,这两种语法是等价的
函数声明
在执行代码时,JS引擎会先执行一遍扫描,把发现的函数声明提升到源代码树的顶部——函数声明提升。
思考以下代码
console.log(sum(10, 10));
let sum = function(num1, num2) {
return num1 + num2;
}
上面的代码会出错,因为这个函数定义包含在一个变量初始化语句中,这意味着代码如果没有执行到赋值的那一行,那么执行上下文中就没有函数的定义,所以上面的代码会出错。(这里就涉及到代码的预编译)
函数表达式
函数表达式看起来就像一个普通的变量定义和赋值,即创建一个函数再把它赋值给一个变量。
如果
function关键字后面没有标识符,则这样创建的函数叫做匿名函数
函数表达式跟js中的其他表达式一样,需要先赋值再使用。它不能像函数声明那样有函数声明提升
理解函数声明与函数表达式之间的区别,关键是理解提升。
// 千万不要这样做
if(condition) {
function doSomething() {...}
} else {
function doSomething() {...}
}
// 这种写法在ES中不是有效的语法,JS引擎会尝试将其纠正为适当的声明。
// 多数浏览器会忽略condition直接返回第二个声明
如果把上面的函数声明换成函数表达式就没问题了
let doSomething;
if(condition) {
doSomething = function () {...}
} else {
doSomething = function () {...}
}
任何时候,只要函数被当做值来使用,它就是一个函数表达式
预编译
JS运行三步:
-
语法分析
-
预编译
-
创建上下文对象
-
找形参和变量声明,将其作为上下文对象的属性,并赋值undefined
-
为形参赋值(值为实参)
-
找函数声明,将其作为上下文对象,并赋值函数体
-
-
解释执行
函数的使用
递归
递归函数通常的形式是一个函数通过名称调用自己
function fn(num) {
return num <= 1 ? 1 : fn(num - 1);
}
尾调用优化
ES6规范新增了一项内存管理优化机制,让JS引擎在满足条件时可以重用栈帧。
“尾调用”即外部函数的返回值是一个内部函数的返回值
function outerFunction() {
return innerFunction(); // 尾调用
}
所谓的优化就是当执行到outerFunction函数体,到达return语句时,即使把第一个栈帧(outerFunction)弹出栈外也没有问题,因为innerFunction的返回值也是outerFunction的返回值。这样无论调用多少次嵌套函数,都只有一个栈帧
尾调用优化的条件 就是确定外部栈帧真的没有必要存在了,涉及的条件如下:
-
代码在严格模式下执行
-
外部函数的返回值是对尾调用函数的调用
-
尾调用函数返回后不需要执行额外的逻辑
-
尾调用函数不是引用外部函数作用域中自由变量的闭包
闭包
闭包指的是那些引用了另一个函数作用域中变量的函数,通常是在嵌套函数中实现的
function foo() {
var a=2;
function baz() {
console.log(a);
}
bar(baz);
}
function bar(fn) {
fn(); // 这就是闭包
}
立即调用的函数表达式(IIFE)
它类似于函数声明,但由于被包含在括号中,所以会被解释为函数表达式。
(function() {...})()
模块模式
模块模式是在单例对象基础上加以扩展,使其通过作用域链来关联私有变量和特权方法
单例对象就是只有一个实例的对象
let singleton = function() {
// 私有变量和私有函数
let privateVariable = 10;
function privateFunction() {
return false;
}
// 特权/公有方法和属性
return {
publicProperty: true,
publicMethod() {
privateVariable++;
return privateFunction();
}
}
}();
在Web开发中,经常需要使用单例对象管理应用程序级的信息。
在模块模式中,单例对象作为一个模块,经过初始化可以包含某些私有的数据,而这些数据又可以通过其暴露的公共方法来访问。
以这种方式创建的每个单例对象都是Object的实例,因为最终单例都由一个对象字面量来表示。不过这无关紧要,因为单例对象通常是可以全局访问的,而不是作为参数传给函数的,所以可以避免使用instanceof操作符确定参数是不是对象类型的需求
模块增强模式
另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类型的实例,但又必须给它添加额外属性或方法的场景
let application = function() {
// 私有变量和私有函数
let components = new Array();
// 初始化
components.push(new BaseComponent());
// 创建局部变量保存实例
let app = new BaseComponent();
// 公共接口
app.getComponentCount = function() {
return components.length;
}
app.registerComponent = function(component) {
if(typeof component == 'object') {
components.push(component);
}
}
// 返回实例
return app;
}
这里创建了一个名为app的变量,其中保存了BaseComponent组件的实例。这是最终要变成application的那个对象的局部版本。在给这个局部变量app添加了能够访问私有变量的公共方法之后,匿名函数返回了这个对象,然后这个对象被赋值给application