六 函数
6.1 函数的概念
函数也是一个对象,它具有其他对象所有的功能,函数中可以存储代码,且可以在需要时调用这些代码
语法:
function 函数名(){
语句...
}
调用函数
调用函数就是执行函数中存储的代码;语法:函数对象()
使用typeof
检查函数对象时会返回function
// 创建一个函数对象
function fn() {
console.log("函数体内容");
}
fn()
console.log(typeof fn);
// 执行结果:
// 函数体内容
// function
6.2 函数的定义方式
6.2.1 函数声明
function 函数名(){
语句...
}
eg:
function fn1(){
console.log("函数声明定义函数");
}
6.2.2 函数表达式
const 变量 = function(){
语句...
}
eg:
const fn2 = function(){
console.log("函数表达式定义的函数");
}
6.2.3 箭头函数
([参数]) => 返回值
例子:
无参箭头函数:() => 返回值
一个参数的:a => 返回值
多个参数的:(a, b) => 返回值
只有一个语句的函数:() => 返回值
只返回一个对象的函数:() => ({...})
有多行语句的函数:
() => {
....
return 返回值
}
()=>{
语句...
}
eg:
const fn3 = () =>{
console.log("箭头函数");
}
fn1()
fn2()
fn3()
/**
执行结果:
函数声明定义函数
函数表达式定义的函数
箭头函数
*/
6.3 参数
6.3.1 形参与实参
形式参数:在定义函数时,可以在函数中指定数量不等的形式参数(形参),在函数中定义形参,就相当于在函数内部声明了对应的变量,但是没有赋值。
function fn(a,b){}
中a与b就是形式参数
实际参数:在调用函数时,可以在函数的()传递数量不等的实参,实参会赋值给其对应的形参。
函数调用时,let name = "Joe";fn(name)
中参数name
就是实际参数,即实参。
-
如果实参和形参数量相同,则对应的实参赋值给对应的形参
-
如果实参多余形参,则多余的实参不会使用
-
如果形参多余实参,则多余的形参为undefined
-
JS中不会检查参数的类型,可以传递任何类型的值作为参数
普通函数接收参数:
function fn(a,b,c) {
console.log("获取到的参数:",a,b,c);
}
fn(1,2) // 获取到的参数: 1 2 undefined
箭头函数接收参数:
const fn = (a,b) =>{
console.log("获取到的参数:",a,b);
}
fn(1,2) // 获取到的参数: 1 2 undefined
当箭头函数中只有一个参数时,可以省略()
const fn2 = a=>{
console.log(a);
}
fn2(1) // 1
6.2.2 参数默认值
function fn(a=1,b=2,c=3){
console.log(a,b,c);
}
fn() // 1 2 3
6.2.3 对象作为参数
修改对象时,如果有其他变量指向该对象则所有指向该对象的变量都会受到影响;
传递参数时
function fn(a){
console.log("函数内获取的初始值:",a.animal);
a.animal = "狗"
console.log("函数内修改后的值:"+a.animal);
}
let a = {animal:"猫"}
fn(a)
console.log("函数外的值:"+a.animal);
/**
执行结果:
函数内获取的初始值: 猫
函数内修改后的值:狗
函数外的值:狗
*/
6.2.4 函数传参原理详解
JavaScript函数传参原理详解——值传递还是引用传递_js函数是值传递还是引用传递-优快云博客
事实上,在js中,不管对于值类型还是引用类类型,都是按值传递的,区别在于,对于值类型,传参发生时,复制的是类型本身的值,而对于引用类型,复制的是类型的地址。
6.2.5 函数作为参数
在JS中,函数也是一个对象(一等函数)。 别的对象能做的事情,函数也可以。
function fn1(a){
console.log("a:",a);
a()
}
function fn2(){
console.log("我是fn2的函数体");
}
fn1(fn2)
6.2.6 函数的返回值
在函数中,可以通过return关键字来指定函数的返回值,返回值就是函数的执行结果,函数调用完毕返回值便会作为结果返回;
任何值都可以作为返回值使用(包括对象和函数之类),
-
如果return后不跟任何值,则相当于返回undefined
-
如果不写return,那么函数的返回值依然是undefined
return一执行函数立即结束
function sum(a,b){
return a+b
}
console.log(sum(5,6)); // 11
箭头函数的返回值:箭头函数的返回值可以直接写在箭头后(简写)
const sum = (a,b)=>a+b
console.log(sum(5,3));
如果直接在箭头后设置对象字面量为返回值时,对象字面量必须使用()括起来
错误写法:
const sum = (a,b)=>{animal:"猫"}
console.log(sum(5,3)); // undefined
正确写法:
const sum = (a,b)=>({animal:"猫"})
console.log(sum(5,3));
PS:这里有一个小面试题
function fn(){
console.log(a);
return
var a = 666
}
fn()
上述函数的执行结果是undefined
而不是 a is not defined
说明函数中return后面var
的变量也存在变量提升特性
6.3 作用域(scope)
6.3.1 参数作用域
作用域指的是一个变量的可见区域
作用域有两种:
-
全局作用域
- 全局作用域在网页运行时创建,在网页关闭时消耗
- 所有直接编写到script标签中的代码都位于全局作用域中
- 全局作用域中的变量是全局变量,可以在任意位置访问
-
局部作用域
- 块作用域是一种局部作用域
- 块作用域在代码块执行时创建,代码块执行完毕它就销毁
- 在块作用域中声明的变量是局部变量,只能在块内部访问,外部无法访问
比如let
定义的参数就具有块作用域,而var
它当前的执行上下文及其闭包(嵌套函数),或者对于声明在任何函数外的变量来说是全局作用域
var a = 123
{
console.log(a); // 123
let b = 0
}
console.log(b); // b is not defined
/**
var 定义的变量a的作用域是全局作用域
let 定义的变量b的作用域是{}包起来的局部作用域
*/
6.3.2 函数作用域
- 函数作用域也是一种局部作用域
- 函数作用域在函数调用时产生,调用结束后销毁
- 函数每次调用都会产生一个全新的函数作用域
- 在函数中定义的变量是局部变量,只能在函数内部访问,外部无法访问
function fn(){
let a = "Hello World"
console.log(a);
}
fn()
console.log(a);
/**
执行结果:
1. Hello World
2. Uncaught ReferenceError ReferenceError: a is not defined
*/
6.3.3 作用域链
当我们使用一个变量时,JS解释器会优先在当前作用域中寻找变量,
-
如果找到了则直接使用;
-
如果没找到,则去上一层作用域中寻找,找到了则使用;
-
如果没找到,则继续去上一层寻找,以此类推
-
如果一直到全局作用域都没找到,则报错 xxx is not defined
后代可以使用父辈的变量,父辈不能使用后代定义的变量
{
let a = 666
{
{
{
{
{
console.log("a:",a);
}
}
}
}
}
}
6.4 window对象
在浏览器中,浏览器为我们提供了一个window对象,可以直接访问,window对象代表的是浏览器窗口,通过该对象可以对浏览器窗口进行各种操作;
除此之外window对象还负责存储JS中的内置对象和浏览器的宿主对象
window对象的属性可以通过window对象访问,也可以直接访问
函数就可以认为是window对象的方法
<!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>
<script>
console.log(window);
</script>
</body>
</html>

在全局中使用
var
声明的变量都会作为window对象的属性保存
<script>
var a = 666
console.log(window.a);
</script>
运行结果:
使用
function
声明的函数,都会作为window对象的方法保存
<script>
function fc(){
console.log("自定义函数");
}
window.fc()
</script>
执行结果:
使用let声明的变量不会存储在window对象中,而存在一个秘密的小地方(无法访问)
var虽然没有块作用域,但有函数作用域
function fn(){
var a = 666
}
console.log(a);
// Uncaught ReferenceError ReferenceError: a is not defined
6.5 提升
6.5.1 变量提升
使用var声明的变量,它会在所有代码执行前被声明,所以我们可以在变量声明前就访问变量。
6.5.2 函数提升
使用函数声明创建的函数,会在其他代码执行前被创建,所以我们可以在函数声明前调用函数。
console.log(a); // Uncaught ReferenceError ReferenceError: a is not defined
由上述代码可见,如果运行时使用了未定义的变量,会报错:a is not defined
console.log(a); // undefined
var a = 666
由上述代码可见,使用var
定义的变量a虽然在使用他之后才定义赋值,但是没有报错not defined
所以可得var
变量的声明提前了。
let声明的变量实际也会提升,但是在赋值之前解释器禁止对该变量的访问
console.log(a); // Uncaught ReferenceError ReferenceError: Cannot access 'a' before initialization
let a = 666
使用let
关键字定义参数a,最后报的错也不是not defined
而是Cannot access 'a' before initialization
ps:let有的功能const也一样
6.6 debug
使用debug
关键字在代码中打一个断点
在浏览器中(index.html):
在vscode中(test.js):
6.7 立即执行函数(IIFE)
见:IIFE(立即调用函数表达式) - MDN Web 文档术语表:Web 相关术语的定义 | MDN (mozilla.org)
立即执行函数是一个匿名的函数,并且他只会调用一次;
可以利用IIFE来创建一个一次性的函数作用域,避免变量冲突的问题。
function
这个关键字,既可以当做语句,也可以当做表达式。
规定:如果function
出现在行首,一律解析成语句。行首是function
关键字时,这一段都是函数定义,不应该以圆括号结尾,所以会报错。当不让function
出现在行首时,这将被理解为一个表达式,最简单的处理方式就是将其放在一个圆括号里。
以圆括号开头,这将被理解为表达式,而不是一个函数定义语句,所以就避免了错误,这就叫“立即执行函数”。
其他的写法:(就是不让function关键字在句首呗?)
(function() {alert('匿名函数')}()) //用括号将整个表达式包起来
(function() {alert('匿名函数')})() //用括号将函数包起来
!function() {alert('匿名函数')}()
+function() {alert('匿名函数')}()
-function() {alert('匿名函数')}()
~function() {alert('匿名函数')}()
void function() {alert('匿名函数')}()
new function() {alert('匿名函数')}()
6.7.1 立即执行函数的作用
- 避免污染全局命名空间:因为我们的程序可能包括很多来自不同源文件的函数和全局变量,因此限制全局变量的数量非常重要。如果我们有一些不再使用的初始化代码,我们可以使用 IIFE 模式。由于我们不会再次重用代码,因此在这种情况下使用 IIFE 会比使用函数声明或者函数表达式更好。
(() => {
// 初始化代码
let firstVariable;
let secondVariable;
})();
// firstVariable 和 secondVariable 变量在函数执行后会被丢弃
- ES6 之前在 For 循环中使用 var
在 ES6 引入 let 和 const 声明和块级作用域之前,我们可以在一些旧代码中看到 IIFE 的以下用法。通过 var 声明变量,只有函数作用域和全局作用域。假设我们创建两个按钮,文本按钮 0 和按钮 1,并且当我们点击它们时,想让它们 alert 0 和 1。下面的代码不能起作用:
for (var i = 0; i < 2; i++) {
const button = document.createElement("button");
button.innerText = `Button ${i}`;
button.onclick = function () {
console.log(i);
};
document.body.appendChild(button);
}
console.log(i); // 2
当点击时,按钮 0 和按钮 1 都会 alert 2,因为 i
是全局的,并且值为 2。在 ES6 之前为了解决这个问题,我们可以使用 IIFE 模式:
for (var i = 0; i < 2; i++) {
const button = document.createElement("button");
button.innerText = `Button ${i}`;
button.onclick = (function (copyOfI) {
return () => {
console.log(copyOfI);
};
})(i);
document.body.appendChild(button);
}
console.log(i); // 2
当点击时,按钮 0 和按钮 1 会 alert 0 和 1,变量 i
是全局的。更简单的是使用 let 声明变量:
for (let i = 0; i < 2; i++) {
const button = document.createElement("button");
button.innerText = `Button ${i}`;
button.onclick = function () {
console.log(i);
};
document.body.appendChild(button);
}
console.log(i); // Uncaught ReferenceError: i is not defined.
… 以上用来入门就可以了
6.8 this
函数在执行时,JS解析器每次都会传递进一个隐含的参数,这个参数就叫做this
this会指向一个对象
this所指向的对象会根据函数调用方式的不同而不同
- 以函数形式调用时,this指向的是window
- 以方法的形式调用时,this指向的是调用方法的对象
- …
通过this可以在方法中引用调用方法的对象
箭头函数没有自己的this,它的this有外层作用域决定且箭头函数的this和它的调用方式无关。
function fn(){
console.log("this:",this);
}
const obj1 = {
animal:"猫",
method:function(){
console.log("this:",this);
}
}
const obj2 = {
animal:"猫",
method:()=>{
console.log("this:",this)
}
}
fn() // this: global {global: global, clearInterval: ƒ, clearTimeout: ƒ, setInterval: ƒ, setTimeout: ƒ, …}
obj1.method() // this: {animal: '猫', method: ƒ}
obj2.method() // this: {}
vscode运行结果:
在vscode里面,也就是nodejs环境下给我返回了个{}
而在浏览器环境下:
有无懂哥知道为什么是{}
?
6.9 严格模式
JS运行代码的模式有两种:
- 正常模式:默认情况下代码都运行在正常模式中,在正常模式,语法检查并不严格,它的原则是:能不报错的地方尽量不报错,而这种处理方式导致代码的运行性能较差
- 严格模式:在严格模式下,语法检查变得严格
- 禁止一些语法
- 更容易报错
- 提升了性能
在开发中,应该尽量使用严格模式,这样可以将一些隐藏的问题消灭在萌芽阶段,同时也能提升代码的运行性能。
"use strict" // 开启全局的严格模式
a = 100
console.log(a); // Uncaught ReferenceError ReferenceError: a is not defined
// "use strict" // 全局的严格模式
a = 100
console.log(a); // 100 不报错
function fn(){
"use strict" // 函数的严格的模式
}