函数就是一些功能或语句的封装。在需要的时候,通过调用的形式,执行这些语句。函数也是一个对象
函数定义
我们使用function
关键字定义函数,中文含义是“函数”、“功能”。可以使用如下方式进行定义。
函数声明
使用函数声明
来创建一个函数。语法:
function 函数名([形参1,形参2...形参N]){ // 备注:语法中的中括号,表示“可选”
// 函数体语句
}
-
函数名:命名规定和变量的命名规定一样,必须符合JS标识符的命名规则。只能是字母、数字、下划线、美元符号,不能以数字开头。
-
圆括号里,是形参列表,可选。即使没有形参,也必须书写圆括号。
-
大括号里,是函数体语句。
函数表达式
使用函数表达式
来创建一个函数。语法:
const fun2 = function([形参1,形参2...形参N]){
语句....
}
-
上面的 fun2 是变量名,不是函数名。
-
函数表达式的声明方式跟声明变量类似,只不过变量里存的是值,而函数表达式里存的是函数。
-
函数表达式也可以传递参数。
从方式二的举例中可以看出:所谓的“函数表达式”,其实就是将匿名函数赋值给一个变量。因为,一个匿名函数终究还是要给它一个接收对象,进而方便地调用这个函数。
使用构造函数
使用构造函数new Function()
来创建一个对象。这种方式,用的少。语法:
const 变量名/函数名 = new Function('形参1', '形参2', '函数体');
注意,Function 里面的参数都必须是字符串格式。也就是说,形参也必须放在字符串里;函数体也是放在字符串里包裹起来,放在 Function 的最后一个参数的位置。
所有的函数,都是 Fuction
的“实例”(或者说是“实例对象”)。函数本质上都是通过 new Function 得到的。
2、函数既然是实例对象,那么,函数也属于“对象”。还可以通过如下特征,来佐证函数属于对象:
(1)我们直接打印某一个函数,比如 console.log(fun2)
,发现它的里面有__proto__
。(这个是属于原型的知识,后续再讲)
(2)我们还可以打印 console.log(fun2 instanceof Object)
,发现打印结果为 true
。这说明 fun2 函数就是属于 Object。
函数调用
调用函数即:执行函数体中的语句。函数必须要等到被调用时才执行。
普通调用
// 写法1(最常用)
函数名();
// 写法2
函数名.call();
对象方式调用
var obj = {
a: 'qianguyihao',
fn2: function() {
console.log('千古壹号,永不止步!');
},
};
obj.fn2(); // 调用函数
立即执行
(function() {
console.log('我是立即执行函数');
})();
通过构造函数调用
function Fun3() {
console.log('hello~');
}
new Fun3();
绑定事件函数
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<div id="btn">我是按钮,请点击我</div>
<script>
var btn = document.getElementById('btn');
//2.绑定事件
btn.onclick = function() {
console.log('点击按钮后,要做的事情');
};
</script>
</body>
</html>
定时器函数
let num = 1;
setInterval(function () {
num ++;
console.log(num);
}, 1000);
函数参数
函数的参数包括形参和实参。形参是函数内的一些待定值。在调用函数时,需传入这些参数的具体值(即实参)。
可以在函数的()
中指定一个或多个参数,也可以不指定参数。多个参数之间用英文逗号隔开。
形参
-
概念:形式上的参数。定义函数时传递的待定值(此时并不知道是什么值)。
-
声明形参相当于在函数内部声明了变量,但并不赋值。也可以说,形参的默认值是 undefined。
实参
-
概念:实际上的参数。调用函数时传递的具体值。实参将传递给函数中对应的形参。
// a, b 是形参,表示待定值
function add(a, b) {
const sum = a + b;
console.log(sum);
}
// 1, 2 是实参,表示传入的具体值。调用函数时,传入实参
add(1, 2);
实际参数和形式参数的个数,可以不同。调用函数时,解析器不会检查实参的数量。
-
如果实参个数 > 形参个数,则末尾的实参是多余的,不会被赋值,因为没有形参能接收它。
-
如果实参个数 < 形参个数,则末尾的形参是多余的,值是 undefined,因为它没有接收到实参。(undefined参与运算时,表达式的运算结果为NaN)
function sum(a, b) {
console.log(a + b);
}
sum(1, 2); // 3
sum(1, 2, 3); // 3
sum(1); //Nan
函数的实参可以是任意的数据类型。调用函数时,解析器不会检查实参类型,所以要注意,是否有可能会接收到非法的参数,如果有可能则需要对参数进行类型检查。
函数返回值
函数体内可以没有返回值,也可以根据需要加返回值。语法格式:return 函数的返回值
return关键字的作用既可以是终止函数,也可以给函数添加返回值。
解释:
(1)return 后的返回值将会作为函数的执行结果返回,可以定义一个变量,来接收该返回值。
(2)在函数中,return后的语句都不会执行。也就是说,函数在执行完 return 语句之后,会立即退出函数。
(3)如果return语句后不跟任何值,就相当于返回一个undefined
(4)如果函数中不写return,则也会返回undefined
(5)返回值可以是任意的数据类型,可以是对象,也可以是函数。
(6)return 只能返回一个值。如果用逗号隔开多个值,则以最后一个为准。
-
break :结束当前的循环体(如 for、while)
-
continue :跳出本次循环,继续执行下次循环(如 for、while)
-
return :1、退出循环。2、返回 return 语句中的值,同时结束当前的函数体内的代码,退出当前函数。
函数隐藏参数
在调用函数时,浏览器每次都会传递进两个隐含的参数:
-
1.函数的上下文对象 this
-
2.封装实参的对象 arguments
function foo() {
console.log(arguments);
console.log(typeof arguments);
}
foo('a', 'b');
函数内的 arguments 是一个类数组对象,里面存储的是它接收到的实参列表。所有函数都内置了一个 arguments 对象,有个讲究的地方是:只有函数才有arguments。
具体来说,在调用函数时,我们所传递的实参都会在 arguments 中保存。arguments 代表的是所有实参。
arguments 的展示形式是一个伪数组。意思是,它和数组有点像,但它并不是数组。它具有以下特点:
-
可以进行遍历;具有数组的 length 属性,可以获取长度。
-
可以通过索引(从0开始计数)存储数据、获取和操作数据。比如,我们可以通过索引访问某个实参。
-
不能调用数组的方法。比如push()、pop() 等方法都没有。
this
this 指向的是一个对象,这个对象我们称为函数执行的 上下文对象。
在ES5语法中,根据函数的调用方式的不同,this 会指向不同的对象:
1、以函数的形式(包括普通函数、定时器函数、立即执行函数)调用时,this 的指向永远都是 window。比如fun();
相当于window.fun();
2、以方法的形式调用时,this 指向调用方法的那个对象
3、以构造函数的形式调用时,this 指向实例对象
4、以事件绑定函数的形式调用时,this 指向绑定事件的对象
5、使用 call 和 apply 调用时,this 指向指定的那个对象
作用域
用域是一个变量或函数的作用范围。作用域在函数定义时,就已经确定了,在 JS 中,一共有两种作用域:全局作用域和局部作用域
全局作用域
直接编写在 script 标签中的 JS 代码,都在全局作用域。全局作用域在页面打开时创建,在页面关闭时销毁。
在全局作用域中有一个全局对象 window,它代表的是浏览器的窗口,由浏览器创建,我们可以直接使用。相关知识点如下:
-
创建的变量都会作为 window 对象的属性保存。比如在全局作用域内写
const a = 100
,这里的a
等价于window.a
。 -
创建的函数都会作为 window 对象的方法保存。
在内部作用域中可以访问外部作用域的变量,在外部作用域中无法访问到内部作用域的变量。
const a = 'aaa';
function foo() {
const b = 'bbb';
console.log(a); // 打印结果:aaa。说明 内层作用域 可以访问 外层作用域 里的变量
}
foo();
console.log(b); // 报错:Uncaught ReferenceError: b is not defined。说明 外层作用域 无法访问 内层作用域 里的变量
局部作用域
(局部作用域):作用于函数内的代码环境。
根据作用域的不同我们可以把变量分为两类:全局变量、局部变量。
全局变量:
-
在全局作用域下声明的变量,叫「全局变量」。在全局作用域的任何一地方,都可以访问这个变量。
-
在全局作用域下声明的变量是全局变量。
局部变量:
-
定义在函数作用域的变量,叫「局部变量」。仅限函数内部访问这个变量。
-
函数的形参也是属于局部变量。
从执行效率来看全局变量和局部变量:
-
全局变量:只有浏览器关闭时才会被销毁,比较占内存。
-
局部变量:当其所在的代码块运行结束后,就会被销毁,比较节约内存。
注意
-
无论是在函数外还是函数内,变量如果未经声明就赋值(意思是,如果不加var/let/const),这个变量是全局变量。
-
如果局部变量和全局变量重名,则在函数内部,变量是以局部变量为准。
-
当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用(就近原则)。如果没有则向上一级作用域中寻找,直到找到全局作用域;如果全局作用域中依然没有找到,则会报错 ReferenceError。
-
在函数中要访问全局变量可以使用 window 对象。(比如说,全局作用域和函数作用域都定义了变量 a,如果想访问全局变量,可以使用
window.a
)
作用域预处理
变量提升
使用 var 关键字声明的变量( 比如 var a = 1
),会在所有的代码执行之前被声明(但是不会赋值)。但是如果声明变量时不是用 var 关键字(比如直接写a = 1
),则变量不会被声明提前。
console.log(a); // undefined
var a = 123;
console.log(b); //Uncaught ReferenceError: a is not defined。
b = 123; //此时a相当于window.a
foo();
function foo() {
if (false) {
var i = 123;
}
console.log(i); // undefined
} // 这个例子,再次说明了:变量 i 在函数执行前,就被提前声明了,只是尚未被赋值。
函数提升
使用函数声明
的形式创建的函数function foo(){}
,会被声明提前。也就是说,整个函数会在所有的代码执行之前就被创建完成。所以,在代码顺序上,我们可以先调用函数,再定义函数。
fn1(); // 虽然 函数 fn1 的定义是在后面,但是因为被提前声明了, 所以此处可以调用函数
function fn1() {
console.log('我是函数 fn1');
}
使用函数表达式
创建的函数const foo = function(){}
,不会被声明提前,所以不能在声明前调用。
很好理解,因为此时只是变量 foo 被提升了,且值为 undefined,并没有把 function(){}
赋值给 foo。
// 不会报错,可以正常执行函数,正常打印结果
fun1();
// 此时 fun2 相当于 undefined。执行时会报错:Uncaught ReferenceError: Cannot access 'fun2' before initialization
fun2();
// 函数声明,会被提前声明
function fun1() {
console.log('我是 fun1 函数');
}
// 函数表达式,不会被提前声明
const fun2 = function () {
console.log('我是 fun12 函数');
};
函数提升优先于变量提升
在JS的规则中,函数提升优先于变量提升。来看看下面这段代码,你认为打印结果应该如何:(这是一道经典面试题)
fun(); // 打印 B
// 变量提升
var fun = function () {
console.log('A');
};
// 函数提升
function fun() {
console.log('B');
}
fun(); // 打印 A
预编译
在讲预编译前,我们先来普及下面两个规律
规律1:任何变量,如果未经声明就赋值,此变量是属于 window 的属性,而且不会做变量提升。(注意,无论在哪个作用域内赋值)
比如说,如果我们直接在代码里写 console.log(a)
,这肯定会报错的,提示找不到 a
。但如果我直接写 a = 100
,这就不会报错,此时,这个 a
就是 window.a
。
规律2:一切声明的全局变量,全是window的属性。(注意,这里说的是在全局作用域内声明的全局变量,不是说局部变量)
比如,当定义 var a = 200
时,这个 a
就是 window.a
。
由此,我们可以看出:window 代表了全局作用域
function foo() {
var a = b = 100; // 连续赋值
}
foo();
console.log(window.b); // 在全局范围内访问 100
console.log(b); // 在全局范围内访问 b,但是前面没有加 window 这个关键字 100
console.log(window.a); // 在全局范围内访问 a undefined
console.log(a); // 在全局范围内访问 a,但是前面没有加 window 这个关键字 报错
闭包
我们知道,变量根据作用域的不同分为两种:全局变量和局部变量。
-
函数内部可以访问全局变量和局部变量。
-
函数外部只能访问全局变量,不能访问局部变量。
-
当函数执行完毕,本作用域内的局部变量会销毁。
在有些场景下,我们就是想要在函数外部访问函数内部作用域的局部变量,那要怎么办呢?这就引入了闭包的概念。
闭包(closure)的概念
闭包:如果外部作用域有权访问另外一个函数内部的局部变量时,那就产生了闭包。这个内部函数称之为闭包函数。注意,这里强调的是访问局部变量。
function fun1() {
var a = 10;
function fun2() {
console.log(a)
}
return fun2();
}
fun1() // 10
上方代码中,外部作用域(即全局作用域) 访问了函数 fun1 中的局部变量,那么,在 fun1 中就产生了闭包,函数 fun1是闭包函数。
全局作用域中,并没有定义变量a。正常情况下作为函数内的局部变量 a,无法被外部访问到。但是通过闭包,我们最后还是可以在全局作用域中拿到局部变量 a 的值。
注意,闭包函数是fun1,不是fun2。fun2在这里的作用是让全局作用域访问到变量a,fun2只是一个桥梁。
闭包的生命周期
-
产生:内部函数fn1被声明时(即被创建时,不是被调用时)就产生了。
-
死亡:嵌套的内部函数成为垃圾对象时。(比如fun1 = null,就可以让 fun1 成为垃圾对象)
闭包的表现形式
将一个函数作为另一个函数的返回值
function fn1() {
var a = 2
function fn2() {
a++
console.log(a)
}
return fn2
}
var f = fn1(); //执行外部函数fn1,返回的是内部函数fn2
f() // 3 //执行fn2
f() // 4 //再次执行fn2
将函数作为实参传递给另一个函数调用
在定时器、事件监听、Ajax 请求、Web Workers 或者任何异步中,只要使用了回调函数,实际上就是在使用闭包。
function showDelay(msg, time) {
setTimeout(function() { //这个function是闭包,因为是嵌套的子函数,而且引用了外部函数的变量msg
alert(msg)
}, time)
}
showDelay('qianguyihao', 2000)
上面的代码中,闭包是里面的function,因为它是嵌套的子函数,而且引用了外部函数的变量msg。
闭包的作用
-
作用1:延长局部变量的生命周期。
-