函数
函数本质是对象,每个函数都是 Function 对象的实例,所以函数名是指向函数对象的指针
函数声明(末尾不需要分号):
function f(a, b){
return a+b;
}
函数表达式:
let f = function(a, b) {
return a+b;
};
箭头函数:
let f = () => {
return a+b;
}
构造函数(不推荐,性能差):
let f = new Function("a", "b", "return a+b");
箭头函数
箭头函数相比函数声明,更简洁,任何函数声明都可以用箭头函数表示
如果只有一个参数,括号可以省略:
let double = x => {return x * 2};
如果函数体只返回一个表达式或者一句赋值语句,可以省略 return 和大括号,会隐式地返回这行代码的值:
let sum = (a, b) => a + b;
函数名
函数名就是指向函数的指针,所以函数可以有多个函数名:
function f() {
cnosole.log("hi");
}
let f2 = f;
f(); // hi
f2(); // hi
所有函数对象都有一个只读的属性 name,用于表示函数名,如果是匿名函数,那么 name 的值为空字符串,如果是使用 Function() 创建的,name 值为 "anonymous"
对于 get set 函数,需要加上 get set 前缀:
let dog = {
age: 1,
get age() {
return this.age;
},
set age(newAge) {
this.age = newAge;
}
}
let propertyDescriptor = Object.getOwnPropertyDescriptor(dog, "age");
console.log(propertyDescriptor.get.name); // get age
console.log(propertyDescriptor.set.name); // set age
理解参数
ES 中没有验证命名参数(形参)的机制,形参的作用仅在于方便理解和操作,所以在定义函数时,可以不写形参,而是通过 arguments 对象接收参数。arguments 是一个类数组对象,用于存储所有的参数
通过 arguments 打印所有参数和参数个数:
function f() {
for(let i = 0;i<arguments.length;i++){
console.log(arguments[i]);
}
// 参数个数
console.log(arguments.length)
}
注意:
- 虽然 ES 没有提供参数的重载,但是可以通过 arguments 判断参数个数,从而实现重载
- arguments 可以和命名参数在函数中同时使用
- 如果通过 arguments 改变参数的值,这个该改变会自动同步到命名参数上(但是 arguments 和命名参数并不指向同一个内存地址)
- arguments 的长度由传入的参数决定,而不是命名参数的个数决定。因此假设有两个命名参数,但是你只传了一个参数,尝试通过 arguments[1] 修改第二个参数的值并不会同步到第二个命名参数,因为 arguments[1] 并不存在
箭头函数中的参数
箭头函数中无法使用 arguments,会报错。
可以在箭头函数外包一层普通函数,这样就可以使用 arguments 了
没有重载
ES 中不像 Java 一样,没有重载,因为 ES 的函数参数没有签名
如果像 Java 中一样尝试重载(两个同名函数,参数个数不一样),第二个函数的定义会覆盖第一个函数的定义,这是由于函数名就是指向函数体的指针,重复定义会修改指针的指向
默认参数值
ES5 之前,只能通过在函数中判断命名参数是否是 undefined,如果是(说明没有传入这个参数),则为这个命名参数设置默认值:
function f(name){
name = (typeof name !== 'undefined') ? name : 'Lin'
...
}
ES6 支持了显示定义参数默认值:
function f(name = 'Lin'){
...
}
如果没有传入参数,那么 name 取默认值 Lin
注意:
- 可以故意传入 undeified,以取默认值
- 命名参数取默认值并不会改变 arguments 的值,修改命名参数也不会影响 arguments,arguments 始终以传入的参数为准
- 默认参数可以是引用类型(比如函数、数组、对象),只有在函数被调用时,才会执行默认参数(如果默认参数是函数类型)
默认参数作用域
默认参数调用的顺序:
function f(name = 'Lin', nick = 'allan'){
...
}
以上函数使用默认参数时,可以理解为:
function f(){
let name = 'Lin',
let nick = 'allan'
...
}
因此参数默认值可以调用之前的命名参数:
function f(name = 'Lin', nick = name){
...
}
但是顺序相反就不可以,参数的初始化遵循暂时性死区,即前面定义的参数不能引用后面的参数,会报错
命名参数只能存在自己的作用域中,无法引用函数内部的变量作为默认值:
function f(name = nick){
let nick = 'allan'
...
}
不传参调用这个函数会报错
参数扩展与收集
扩展运算符,可以简洁地操作集合数据
参数扩展
可以通过扩展运算符,简洁地传入参数:
let arg = [1, 2, 3, 4];
function f(){
for(let i = 0;i < arguments.length; i++){
console.log(arguments[i])
}
}
// 相当于传入 1, 2, 3, 4
f(...arg);
// 1
// 2
// 3
// 4
扩展运算符可以将可迭代对象进行拆分,并将每一个拆分结果作为一个单独的参数传入,也可以调用 apply() 方法实现同样效果(第一个参数是 this 的值,第二个参数是数组或类数组参数):
f.apply(null, arg);
收集参数
扩展操作符也可以用在函数的命名参数上,用于将多个参数合并为一个数组:
function f(...args){
console.log(args)
}
f(1, 2, 3, 4)
// [1, 2, 3, 4]
扩展操作符参数只能作为最后一个命名参数,它之后不可以有其他命名参数
箭头参数也可以通过收集参数来实现类似于 arguments 的功能
函数声明与函数表达式
函数声明提升:函数声明会被先读取,并在执行上下文中生成函数定义,所以之前的代码可以调用函数
函数表达式不会提升,所以如果在函数表达式之前调用函数,那么会报错,无论是 var 还是 let 声明的函数调用都会报错(var 变量声明的函数表达式可以被打印,但是不能被调用)
函数作为值
函数名是一个变量,所以函数可以作为值在任何地方,可以作为参数,或返回值
应用:根据某一个属性排序对象数组
let data = [
{name: 'Lin', age: 23},
{name: 'Zhang', age: 22}
]
function comparation(prop, order) {
return function(obj1, obj2){
let val1 = obj1[prop]
let val2 = obj2[prop]
if(val1 < val2){
return -1
} else if (val1 > val2){
return 1
} else {
return 0
}
}
}
// 根据年龄属性升序排序对象数组
console.log(data.sort(comparation("age")))
函数内部
arguments
arguments 有一个属性 callee,是一个指针,指向 arguments 所在函数,在递归中可以解耦函数名和函数逻辑,如下示例:
// 阶乘的递归函数
function factorial(num) {
if(num == 1) return 1;
return num * factorial(num - 1);
}
等同于:
function factorial(num) {
if(num == 1) return 1;
return num * arguments.callee(num - 1);
}
通过 arguments.callee() 等同于调用了函数,通过这个属性实现函数名和函数逻辑的解耦:
let newFuncName = factorial
factorial = function() {
return 0
}
console.log(newFuncName(3))
// 6
以上代码将变量 newFuncName 指向 factorial 函数,然后又将 factorial 指向了一个新函数,最后通过 newFuncName 调用原来的阶乘函数,正是因为 callee 的存在,递归函数可以正确执行(如果没有使用 callee,那么执行结果会是 0 )
this
this 是一个指针
标准函数
this 引用的是把函数作为方法调用的上下文对象,this 的指向在函数执行时才能被确定:
window.color = "red"
let o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
// 调用方法的上下文对象是 window
sayColor()
// red
o.sayColor = sayColor
// 调用方法的上下文对象是 o
o.sayColor()
// blue
箭头函数
this 函数指向箭头函数所在的上下文,将上面的例子改为箭头函数,执行结果输出的均为 red,因为 this 始终指向箭头函数所在的上下文(window)
因此事件的回调函数使用箭头函数,可以得到当前函数所在的上下文,而如果回调函数采用普通函数,那么 this 会指向 window 对象的上下文:
function f() {
let name = 'Lin'
setTimeout(()=> console.log(this.name), 1000)
}
f()
// Lin
function f() {
let name = 'Lin'
setTimeout(function(){console.log(this.name)}, 1000)
}
f()
// undefined
caller
caller 是函数的一个属性,caller 指向调用当前函数的函数:
function outer(){
inner()
}
function inner(){
console.log(arguments.callee.caller)
}
outer()
/*
ƒ outer(){
inner()
}
**/
new.target
ES6 新增了 new.target 来判断一个函数是被当作构造函数调用,还是被当作普通函数调用
普通调用:new.target 的值为 undefined
new 调用:new.target 的值为被调用的构造函数
function f(){
// 必须通过 new 调用这个函数
if(!new.target){
throw 'f must be instantiated using "new"!'
}
}
函数属性和方法
函数是一种特殊的对象
属性
length
保存函数定义的命名参数的个数
prototype
引用类型的所有实例都可以共享 prototype 上保存的方法
方法
apply()
用指定的作用域调用函数
两个参数:this 的值,一个参数数组(数组实例或 arguments 对象)
call()
作用和 apply() 一样
多个参数,this 的值,其他参数是要传入函数的所有参数
call() 和 apply() 的优势在于可以将任意对象设置为任意函数的作用域,这样可以让一个对象调用一个一个它当中并未定义的方法
bind()
bind() 的参数是一个对象,可以将函数的 this 指向这个对象的作用域,然后返回这个绑定完成的新函数
bind() 不同于 apply() 和 call(),它并不会立即调用函数,而是返回一个指定 this 的新函数
window.color = 'red'
let o = {
color: 'blue'
}
function sayColor() {
console.log(this.color)
}
let oSayColor = sayColor.bind(o)
oSayColor()
// blue
继承的方法
toString() 和 tolocaleString() 返回函数的代码,具体格式因浏览器而异,valueOf() 返回函数本身,浏览器解释器可能会修改代码,因此应该只在调试时使用这些方法
函数表达式
函数表达式不会提升,这是它和函数声明最大的区别。
条件语句中使用函数声明会产生非预期的结果:
if(condition){
function a(){
...
}
}else {
function a(){
...
}
}
这段代码意图通过 condition 的值,来创建不同的函数,但是 JS 引擎会将函数声明提升,多数浏览器会忽略 condition 直接返回第二个声明,fixfox 相反。但是将函数声明换成函数表达式就不会有这样的问题了。
函数表达式,将一个普通变量指向一个 匿名函数(也叫 Lambda 函数),匿名函数也可以作为一个函数的返回值:
function f(){
return function(){
...
}
}
这种情况,只要匿名函数被当作一个值使用,就是一个函数表达式
递归
在介绍 callee 的时候,举了一个例子,例子中的阶乘函数就是一个递归函数,并且通过 arguments.callee 也可以解决递归函数被重命名后产生的问题,但是在严格模式下,不能使用 arguments.callee,可以使用 命名函数表达式 解决这个问题:
const factorial = (function f(num){
if(num == 1) return 1
return num * f(num - 1)
});
将一个函数声明用括号包裹(表示这是一个表达式),再将其赋值给 factorail,这种做法可以随意改变函数名 factorial,因为最终递归调用的函数名总是 f
尾调用优化
ES6 新增的内存优化机制
以这段代码为例:
function outer() {
return inner()
}
优化前:
- 执行 outer() 函数体,将其推入栈帧
- 执行 inner() 函数体,将其推入栈帧
- 计算 inner() 的返回值,inner() 函数体出栈
- 计算 outer() 的返回值(就是 inner() 的返回值),返回结果
- outer() 出栈
优化后:
- 执行 outer() 函数体,将其推入栈帧
- 引擎发现由于 outer() 返回值就是 inner() 的返回值,所以 outer() 出栈
- inner() 入栈
- 计算出 inner() 的返回值,返回结果
- inner() 出栈
优化之后,对于尾调用的情况,减少了大量占用的栈内存,上面的例子只是嵌套了一层,如果是递归函数,无论嵌套多少次,优化后的栈内存中永远只有一个栈帧
优化条件
条件如下:
- 严格模式
- 外部函数的返回值是对内部函数的调用
- 尾部调用函数返回后不需要执行额外的逻辑
- 尾调函数不是引用外部函数作用域中自由变量的闭包
外部函数的返回值是对内部函数的调用:
// 不优化,没有返回
function outer(){
inner()
}
// 不优化,没有直接返回函数
function outer(){
let innerResult = inner()
return innerResult
}
尾部调用函数返回后不需要执行额外的逻辑:
// 不优化,执行额外的逻辑
function outer(){
return inner().toString()
}
尾调函数不是引用外部函数作用域中自由变量的闭包:
// 不优化,尾调是一个闭包
function outer(){
let foo = 'bar';
function inner(){return foo;}
return inner();
}
利用尾调用优化进行代码优化
斐波那契数列:
function fib(n){
if(n<2) return n
return fib(n-1) + fib(n-2)
}
以上代码返回的函数执行了额外的逻辑(相加),不能进行尾调用优化
优化后:
"use strict";
function fib(n){
return fibImpl(0, 1, n)
}
// 可以尾调用优化的递归,第一个参数是 n-2 的值,第二个参数是 n-1 的值,第三个参数是剩余循环的次数
function fibImpl(a, b, n){
if(n === 0) return a;
return fibImpl(b, a + b, n - 1)
}
// 尾调用优化后的递归可以看作是迭代
function fib(n){
// fib(n-2) 的初始值,即 fib(0)
let n2 = 0
// fib(n-1) 的初始值,即 fib(1)
let n1 = 1
// 从 fib(2) 开始计算每个 fib(x) 的结果,循环 n 次,计算到 fib(n+2)
for(let i = 0;i < n;i++){
// 利用递推关系更新 fib(n-1) 和 fib(n-2)
let temp = n1 + n2
n2 = n1
n1 = temp
}
// 返回 fib(n-2) 的值,即 n2
return n2
}
闭包
闭包(closure)是指引用了另一个函数作用域中变量的函数
实例:构造一个比较对象指定属性大小的函数
function createComparationFunction(property){
return function(obj1, obj2){
let val1 = obj1[property];
let val2 = obj2[property];
if(val1 > val2){
return val1;
}else{
retrurn val2;
}
}
}
概念:
- 变量对象:全局上下文中包含其变量的对象,在代码执行期间一直存在
- 活动对象:函数局部上下文中包含其变量的对象,在函数执行期间存在
回顾:作用域链,函数对象的 [[scope]] 会指向作用域链(一个包含指针的列表),而每个指针又会指向活动对象(函数的局部上下文)。在上例中有三个活动对象:全局对象、createComparationFunction 对象,匿名函数对象
闭包:上例中,匿名函数就是一个闭包,它的作用域链从头到尾分别是:闭包的活动对象、createComparationFunction 的活动对象、全局变量对象。由于闭包需要用到 createComparationFunction 函数中的 property 变量,所以 createComparationFunction 函数执行完毕后,它的活动对象不会被销毁,因此会占用内存
创建并销毁闭包:
// 创建闭包
let compare = createComparationFunction('name');
// 使用闭包
let result = compare({name: '1'}, {name: '2'});
// 销毁
compare = null;
减少闭包的使用,尽管 V8 已经尽量优化,这样可以减少内存占用
this 对象
匿名函数的 this 不会绑定到某个对象,而是指向 window 对象
实例:
window.identify = 'window';
let obj = {
identify: 'obj',
getIdentifyFunc(){
return function(){
console.log(this.identify);
}
}
}
obj.getIdentifyFunc()();
// window
根据结果可看出,因为调用的是匿名函数,this 指向了 window 对象
如果想打印 obj 中的 identify,可以利用变量 that 保存 obj 上下文的 this 对象,并使用闭包:
window.identify = 'window';
let obj = {
identify: 'obj',
getIdentifyFunc(){
let that = this;
return function(){
console.log(that.identify);
}
}
}
obj.getIdentifyFunc()();
// obj
特殊情况下的 this 指向不同:
window.identify = 'window';
let obj = {
identify: 'obj',
getIdentifyFunc(){
console.log(this.identify);
}
}
obj.getIdentifyFunc();
// obj
(obj.getIdentifyFunc)();
// obj
(obj.getIdentifyFunc = obj.getIdentifyFunc)();
// window
// 因为对函数进行了赋值,解除了函数 this 对于 obj 的绑定
内存泄漏
在 IE9 之前,因为采用了引用计数的垃圾回收方式,如果使用了闭包,引用的外部变量一直不会被销毁:
function handler(){
let element = document.getElementById('someElement');
element.onclick = () => {console.log(element.id)};
}
这个例子中,箭头函数中引用了 element 变量,这阻止了 element 的引用计数值的清零,因此可能会导致内存泄漏
function handler(){
let element = document.getElementById('someElement');
let id = element.id;
element.onclick = () => {console.log(id)};
element = null;
}
如上修改后,可以解除箭头函数内部对 element 的直接引用,并在打印执行结束后,通过 element = null 解除对对象的引用,这样 element 的引用计数才得以被清零
立即调用的函数表达式
IIFE (Immediately Invoked Function Expression)
示例:
(function(){
// 模拟块级作用域
})();
ES6 之前没有块级作用域,通过 IIFE 来模拟块级作用域,ES6 中就没有必要了:
{
// 块级作用域
}
关于 for 循环中使用 var 声明产生的问题:
for(var i = 0; i < divs.length; ++i){
divs[i].addEventListener('click', funcition(){
console.log(i)
})
}
以上代码给每个 div 添加了点击事件,并且点击时会打印 i 的值,预期结果是点击第 i 个 div 会打印 i ,但结果是点击任何 div 都会打印 divs.length。这是因为循环的变量 i 是用 var 声明的,所以 i 是一个全局变量,当点击事件触发匿名函数时,这个匿名函数会查找当前的变量 i 的值,因为此时循环已经结束,i 的值是 divs.length,所以点击任何 div 都会打印 divs.length。
为了让每个 div 点击事件触发的匿名函数都有独立的 i,可以使用 IIFE 来锁定 i 的值:
for(var i = 0; i < divs.length; ++i){
divs[i].addEventListener('click', (funcition(count){
return function(){
console.log(count)
}
})(i));
}
ES6 之后,可以通过 let 实现:
for(let i = 0; i < divs.length; ++i){
divs[i].addEventListener('click', funcition(){
console.log(i)
})
}
let 声明的变量作为 for 循环的索引,会在每次循环时创建独立的变量,这样就可以使每一个匿名函数的 i 都是独立的,但如果在循环块作用域外声明,则会失效:
// 在循环外声明就和 var 声明一样了
let i;
for(i = 0; i < divs.length; ++i){
divs[i].addEventListener('click', funcition(){
console.log(i)
})
}
私有变量
私有变量:定义在函数或块中的变量,包括函数参数、局部变量、函数内定义的函数
严格来说,JS 没有私有变量,但是可以通过某些方法来实现类似的功能。
特权方法(privileged method)是能够访问私有变量的方法,可理解为 Java 中的 get/set 方法
function Person(name){
this.getName = function(){
return name;
}
this.setName = function(value){
name = value;
}
}
let person = new Person('lin');
person.getName();
// lin
缺点是每个实例都会重新创建方法,这一点在对象章节中提到过
静态私有变量
为了避免上述问题,可以使用静态私有变量
(function(){
let name = '';
Person = function(value) {
name = value;
}
Person.prototype.getName = function(){
return name;
}
Person.prototype.setName = function(value){
name = value;
}
})();
let person1 = new Person('lin');
person1.getName();
// lin
let person2 = new Person('wang');
person2.getName();
// wang
person1.getName();
// wang
静态,表示每个实例的私有变量都有指向统一个值
这里使用 IIFE 将私有变量 name 隔离,并且将特权方法定义在构造函数的原型上,这样可以让每个实例都共享原型上的特权方法,并且私有变量也是共享的,这是因为构造函数和特权方法都是闭包,所有的实例都引用了同一个私有变量 name。因此,只要通过一个实例修改其私有变量,那么所有其他的实例的私有变量也会被修改。
模块模式
单例对象(singleton),只有一个实例的对象:
let singleton = {
name: value,
method() {
// 方法
}
}
模块模式在单例对象的基础上拓展:
// 这是一个采用模块模式模拟管理组件的函数
let app = function() {
let components = new Array();
// 初始化组件数组
components.push(new BaseComponent());
// 返回对象
return {
// 获取组件数量
getComponentCount(){
return components.length;
}
// 注册新组件
registerComponent(component) {
if(typeof component == 'object') {
components.push(component);
}
}
}
}
这个函数返回的是一个对象,这个对象通过作用域链关联了私有变量,包含了初始化之后的私有变量,并提供公有方法对私有变量进行操作
模块增强模式
增强,是指对象在返回之前需要是某个特定类型的实例
// 这是一个采用模块模式模拟管理组件的函数
let app = function() {
let components = new Array();
// 初始化组件数组
components.push(new BaseComponent());
// 创建增强对象
let comp = new BaseComponent();
// 获取组件数量
comp.getComponentCount = function(){
return components.length;
}
// 注册新组件
comp.registerComponent = function(component) {
if(typeof component == 'object') {
components.push(component);
}
}
// 返回对象
return comp;
}