# JavaScript教程 - 8 函数
什么是函数呢?
先举个栗子:
假设给定两个整数,想获取其中的最大值,那么可以编写代码如下:
// 只是举个栗子,以下代码不是最简洁方式
let num1 = 24;
let num2 = 12;
let maxNum;
if (num1 > num2) {
maxNum = num1;
} else {
maxNum = num2;
}
console.log("较大的数为:" + maxNum); // 较大的数为:24
2
3
4
5
6
7
8
9
10
11
12
一个地方需要这个功能,我在一个地方写了这个功能的代码,100个、1000个地方需要这个功能,那就需要写1000遍。万一这个功能需要修改,修改1000个地方不得崩溃。
所以我们可以将这个功能封装为一个函数,想要使用到这个功能的时候,直接调用这个函数即可。
所以通过函数,提高代码的复用性,减少重复代码,提高开发效率。
我们前面也已经调用过一些函数,例如 console.log()
、alert()
、parseInt()
等,他们都实现了某些特定的功能,也就是功能进行了封装,我们只要调用就可以实现想要的功能,不用再自己编写。
在 JS 中,函数也是对象,所以函数具有对象所以的功能,而且函数还可以封装代码,提供想要的功能。
而现在我们主要学习如何自定义函数。
# 8.1 函数的定义
函数需要先定义,然后我们就可以调用函数了。
# 1 定义函数
定义函数的语法如下:
function 函数名(参数1, 参数2, ...) { // 参数可以省略
// 实现功能的代码
}
2
3
下面定义一个函数,函数实现的功能就是就是打印一行语句:
/**
* 定义一个函数
*/
function sayHello() {
alert('Hello, 欢迎来到For技术栈');
}
2
3
4
5
6
- 上面定义了一个函数,函数实现的功能比较简单,就是输出一句话。
function
关键字用来定义函数,sayHello
是函数的名字,是自定义的,建议按照标识符命名规则,采用首字母小写的命名方式;- 然后是
()
,用来指定参数,这里没有参数,后面会讲,{}
是函数体部分,可以编写函数要实现的功能。
# 2 函数的调用
函数已经定义好了,下面来调用函数:
/**
* 定义一个函数
*/
function sayHello() {
alert("Hello, 欢迎来到For技术栈");
}
// 调用函数
sayHello();
// 再次调用函数
sayHello();
2
3
4
5
6
7
8
9
10
11
12
- 调用函数使用
函数名();
这样的方式来调用,函数可以调用多次,调用一次就是将函数体的代码执行一次。
执行效果如下:
# 3 函数的类型
查看一下函数的类型:
// 定义一个函数
function sayHello() {
alert("Hello, 欢迎来到For技术栈");
}
console.log(typeof sayHello); // function
2
3
4
5
6
- 使用 typeof 查看函数类型为
function
,typeof
操作符返回function
并不是object
,这只是为了区分函数和普通对象,函数只是对象类型的一种特殊形式。
# 4 匿名函数
上面定义函数的方式指定了函数名称,这种方式为具名函数,还有不指定函数名的函数,称为匿名函数。
举个栗子:
// 定义一个函数
let func = function() {
alert("Hello, 欢迎来到For技术栈");
}
// 调用函数
func();
2
3
4
5
6
7
- 上面使用
function(){...}
定义了一个匿名函数,并赋值给 func 变量; - 在调用的时候,通过变量调用即可。
# 5 箭头函数
ES6 引入的箭头函数,是一种更为简洁的定义函数的方式。
举个栗子:
// 定义一个函数
let func = () => {
alert("Hello, 欢迎来到For技术栈");
}
// 调用函数
func();
2
3
4
5
6
7
- 上面通过
() => {}
的方式定义函数,并将函数赋值给变量 func,所以箭头函数也是匿名函数。
如果箭头函数的函数体只有一条语句,还可以省略 {}
。
举个栗子:
// 定义一个箭头函数
let func = () => alert("Hello, 欢迎来到For技术栈");
// 调用函数
func();
2
3
4
5
# 8.2 函数的参数
上面定义的函数没有参数,在实际的使用中,函数一般都有参数,例如我们之前使用的 alert('Hello');
括号中的就是参数,通过传递不同的参数,来让函数针对不同的数据进行处理。
# 1 函数参数
下面来定义一个函数,作用是计算任意两个数的和,所以函数需要接收两个参数,在函数中计算两个参数的和。
代码如下:
/**
* 定义两个数相加的函数
*/
function add(a, b) {
let result = a + b;
console.log("result:", result);
}
// 调用函数
add(1, 2);
2
3
4
5
6
7
8
9
10
- 首先在函数的括号中定义参数,参数个数不限,使用逗号分隔;函数的参数相当于在函数内部定义的局部变量,但是没有初始化。
- 在调用函数的时候,传递参数,会将值传递给函数的参数进行初始化。在上面的代码中,调用
add()
函数,会将1
复制给a
,2
复制给b
。 - 函数的参数叫形参(形式参数),传递的参数叫实参(实际参数,实际执行的参数),不过在实际的开发中,一般说函数的参数就完了。
加深印象,再举个栗子:
function sayHello(name, age) {
console.log(`Hello, I'm ${name}, I'm ${age} years old`);
}
let name = "Doubi";
let year = 13;
// 调用函数
sayHello(name, year); // Hello, I'm Doubi, I'm 13 years old
// 再次调用函数
sayHello('Niubi', 10); // Hello, I'm Niubi, I'm 10 years old
2
3
4
5
6
7
8
9
10
11
- 上面定义了一个
sayHello()
函数,接收两个参数,上面定义实参name和age和函数的形参名字是一样的,但两者没有任何关系,它们之间只是值的传递。
# 2 参数的个数
正常情况下,函数有几个参数,我们传递几个参数就可以了,一一对应,会将传递的值和函数的参数进行对应。
但是 JS 比较骚,传递的参数(实参)和函数的个数(形参)可以不相同。
举个栗子:
/**
* 定义两个数相加的函数
*/
function add(a, b) {
console.log('a =', a, ", b =", b)
let sum = a + b;
// console.log("sum:", sum);
}
// 调用函数
add(); // a = undefined , b = undefined
add(1); // a = 1 , b = undefined
add(1, 2, 3, 4); // a = 1 , b = 2
2
3
4
5
6
7
8
9
10
11
12
13
- 从上面的执行可以看出,如果实参个数多于形参个数,多的实参会被忽略;如果实参个数少于形参个数,形参的值为 undefined,没有初始化。
虽然 JS 传递的参数的参数和函数的参数个数可以不同,但是强烈建议你猥琐发育,别浪!
# 3 参数的类型
JS 是弱类型语言,定义的函数的参数也没有类型约束,所以传递什么类型的数据都是可以的。
举个栗子:
/**
* 定义两个数相加的函数
*/
function add(a, b) {
let sum = a + b;
console.log("sum:", sum);
}
// 调用函数
add(2, 3); // sum: 5
add('Hello', 'For技术栈'); // sum: HelloFor技术栈
add(true, 2); // sum: 3
add(null, 2); // sum: 2
add(2, 'abc'); // sum: 2abc
2
3
4
5
6
7
8
9
10
11
12
13
14
- 上面函数实现的是两个参数的相加,所以按照算数运算符号进行运行。
- 所以在实际的开发中,一定要注意参数的类型。
# 4 箭头函数的参数
箭头函数的参数,在有且仅有一个参数的时候,括号 ()
可以省略。
举个栗子:
let func = (a) => {
console.log("a:" + a);
}
// 一个参数的时候,()可以省略,可以写为:
let func = a => {
console.log("a:" + a);
}
2
3
4
5
6
7
8
# 5 参数默认值
函数的参数可以指定默认值。
举个栗子:
function add(a=10, b=20, c=30) {
console.log('sum:' + (a + b + c));
}
add(1); // 没有传递b和c的值,b默认为20,c默认为30
add(1, 2); // 没有传递c的值,c的值默认为30
add(1, 2, 3);
2
3
4
5
6
7
- 参数指定默认值后,如果不传递参数,将使用默认值。
# 6 参数值传递
在 JavaScript 中调用函数传递参数是值传递,不是地址传递!
什么意思呢?举个栗子:
我们编写代码,交换 a 和 b 两个变量的值,代码如下:
let a = 3;
let b = 5;
// 交换a和b的值
let temp = a; // 需要一个中间变量
a = b;
b = temp;
console.log(a); // 5
console.log(b); // 3
2
3
4
5
6
7
8
9
10
交换两个变量这个功能,可能其他地方也会用到,于是写一个函数来实现:
/**
* 交换两个变量
*/
function swap(a, b) {
let temp = a;
a = b;
b = temp;
}
let a = 3;
let b = 5;
// 调用交换函数
swap(a, b);
console.log(a); // 3
console.log(b); // 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
运行代码,发现 a
和 b
的值并没有被交换,不科学啊,为什么呢。
首先执行:let a = 3; let b = 5;
,函数中的变量会在栈中开辟内存空间:
继续执行,调用 swap 函数,swap 函数的形参 (a, b)
和变量 a
和 b
根本就不是同一个变量,只是这里恰巧名称相同而已,只是将变量a和b的值赋值给了 swap 函数的形参。
然后执行 swap 函数中的代码,创建了 temp 变量,调换了函数中 a
和 b
的值,所以并没有改变变量a和b的值。
再看一段代码:
// 修改a的属性
function change(a) {
a.name = 'niubi';
}
let a = {name:'doubi', age:18}
change(a);
console.log(a.name); // niubi
2
3
4
5
6
7
8
为什么上面的函数又能修改a的属性值呢?
这是因为函数的参数是值传递,但是变量 a 是引用类型变量,存储的是地址值,所以传递的也是地址值。
调用方法后,会将 a 的值传递给函数参数a,那么函数参数 a 也指向堆中的对象:
然后修改参数 a 的值,也就修改了变量 a 的值:
所以这里也是值传递,只是传递的是引用的地址,所以指向的是堆中同一块内存地址。
# 8.3 函数的返回值
上面写的函数都是没有返回值的,计算两个数的和,我想拿这个和继续处理其他的逻辑,目前是不行的。
所以有时候,我们是需要函数将处理的结果返回给我们,例如 let num = parseInt(str);
函数接收一个字符串,返回一个数值。
# 1 返回值
函数如果要返回值,使用 return
关键字。
举个栗子:
function add(a, b) {
let sum = a + b;
return sum; // 使用 result 返回值
}
let result = add(2, 3); // 调用函数
console.log(result);
2
3
4
5
6
7
- 上面使用 return 将计算的结果返回;
- 在调用函数时,我们可以使用变量接收返回的值。
我们也可以简化上面的代码:
function add(a, b) {
return a + b; // 使用 result 返回值
}
console.log(add(2, 3)); // 直接输出调用结果
2
3
4
5
函数的返回值需要定义变量来接收,不过,如果你只是调用,不接收结果,也是没有问题的:
# 2 返回值的类型
任何类型的数据都可以作为函数的返回值,包括对象和函数。如果 return 不写返回值,那么返回 undefined,如果不写 return,返回值也是 undefined。
举个例子:
function add(a, b) {
let sum = a + b;
return; // 没有返回任何值
}
let result = add(2, 3);
console.log(result); // undefined
2
3
4
5
6
7
- 上面的代码中,函数直接return,没有返回值,那么函数的返回结果是 undefined。
同样,如果函数不写 return,调用函数,接收函数的返回结果,也是 undefined。
如下:
function add(a, b) {
let sum = a + b;
}
let result = add(2, 3);
console.log(result); // undefined
2
3
4
5
6
# 3 终止函数执行
return 表示终止函数的执行,当执行 return 后,会直接返回,表示函数执行完成。
例如,我们写一个除法的方法,检查一下除数是否是 0,如果是0,直接返回:
function divide(a, b) {
if (b == 0) {
return;
}
return a / b;
}
let result = divide(5, 0);
console.log(result); // undefined
2
3
4
5
6
7
8
9
10
- 在上面的代码中,先判断除数是否是0,如果是0,则直接将函数返回,函数将终止执行。
所以如果 return 语句后面有代码,则无法被执行。
举个栗子:
function add(a, b) {
return a + b; // 这里函数就返回了
console.log(a + b); // 前面已经return,这里无法被执行到
alert(a + b);
}
let result = add(2, 3);
console.log(result); // 5
2
3
4
5
6
7
8
9
- 函数执行到 return 就返回了,后面的代码无法被执行到。
# 4 箭头函数的返回值
如果箭头函数的函数体只有一句 return 语句,那么可以省略 {}
。
举个栗子:
let add = (a, b) => a + b;
console.log(add(2, 3)); // 5
2
但是这样有一个问题,如果返回的数据是对象类型,编译器没办法区分是函数的 {}
还是 对象的 {}
。
let obj = (a, b) => {name: name, age: age}; // 无法区分{}是函数的还是对象的。
需要使用小括号()
,括起来,如下:
let func = (name, age) => ({name: name, age: age});
let person = func('doubi', 18);
console.log(person.name); // doubi
2
3
# 8.4 函数的嵌套调用
函数的嵌套调用就是一个函数可以调用另外一个函数,另外一个函数还可以继续调用其他的函数,依此类推。
举个栗子:
下面定义了2个函数,funA() 和 funB(),并在main()函数中调用了funA() ,然后在funA()中调用了 funB()。
function funB() {
console.log("----b");
}
function funA() {
console.log("----a1");
funB();
console.log("----a2");
}
funA();
2
3
4
5
6
7
8
9
10
11
执行结果:
----a1 ----b ----a2
我们会发现在 funA() 中调用 funB() 后,funB() 执行完成,重新回到了 funA() 继续执行。
# 8.5 函数作为对象属性
对象的属性除了是基本数据类型和引用数据类型,还可以是函数。
当使用函数作为对象的属性的时候,一般称之为对象的方法,本质上函数和方法是一样的。
举个栗子:
let person = {};
person.name = 'doubi';
person.age = 13;
person.run = function(count) {
alert(`奔跑吧, 骚年! 跑个${count}米`);
}
person.run(100); // 奔跑吧, 骚年! 跑个100米
2
3
4
5
6
7
8
9
- 在上面的代码中,将一个函数赋值给对象的属性,然后可以通过对象来调用这个方法。
- 在前面通过
console.log()
也是通过对象来调用方法。
# 8.6 作用域
什么是作用域?
作用域就是变量的可访问范围,简单来说就是在哪里可以访问到变量。
在 JS 中作用域分为全局作用域和局部作用域,局部作用域又可以分为块级作用域、函数作用域。
# 1 全局作用域
全局作用域中声明的变量,在代码的任何地方都可以访问。
举个栗子:
let a = 123;
{
let b = 456;
console.log(a); // 可以访问a
}
function test() {
let c = 789;
console.log(a); // 可以访问a
// console.log(b); // 无法访问b
}
test();
console.log(a); // 也可以访问
// console.log(b); // 无法访问b
// console.log(c); // 无法访问c
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- 在上面的代码中,变量
a
是全局作用域,直接在<script>
标签中声明的代码就是在全局作用域中,可以在任何地方访问。 - 变量b 和变量c 只能在所在的代码块或函数中访问,它们是局部作用域。
- 全局作用域是在网页运行的时候创建,在网页关闭的时候销毁;而块级作用域只有在代码块运行的时候创建,运行完成就销毁;函数作用域会在函数每次被调用的时候创建,每次都是创建新的作用域,函数调用完成就销毁了。
注意:不使用 var/let/const
声明的变量,会默认变成全局变量(这是一个坑)。
举个栗子:
function test() {
a = 123; // 错误做法:没有声明
}
test();
console.log(a); // 输出 123(不推荐)
2
3
4
5
6
- 非常不推荐
# 2 块级作用域
ES6 引入了 let
和 const
,它们拥有块级作用域。
块级作用域:在一对花括号 {}
中声明的变量,只在这对大括号中有效。
{
let x = 10;
const y = 20;
console.log(x, y); // 正常访问
}
console.log(x); // 报错:x is not defined
2
3
4
5
6
而 var
没有这个能力,会被提升到函数作用域或全局作用域。
if (true) {
var a = 123;
}
console.log(a); // 输出123,var不受块级作用域限制
2
3
4
5
- 上面的变量a 使用 var 声明,没有块级作用域,因为变量a 也没有在函数中,所以被提升到全局作用域。
# 3 函数作用域
函数内部声明的变量,只能在该函数内部访问。
function test() {
var a = 123;
console.log(a); // 正常访问
}
test();
console.log(a); // 报错:a is not defined
2
3
4
5
6
7
- 上面的变量a 只能在函数内部访问。
因为 var
声明的变量没有块级作用域,所以在函数中声明的变量会被提升为函数作用域(不是块级的)。
function test() {
if (true) {
var a = 5;
}
console.log(a); // 输出 5,不报错
}
test();
console.log(a); // 无法访问
2
3
4
5
6
7
8
9
- 虽然 var 没有块级作用域,但有函数作用域;上面的变量a只能在函数中访问。
# 4 作用域链
当访问一个变量时,JS 会在当前作用域查找变量,如果找不到,会去父一级的作用域查找,如果找到就使用,还找不到,继续向上查找,直到在全局作用域查找,如果还找不到,就报错了xx is not defined
,这样就形成了一条链子,也就是作用域链。
let a = 1;
function outer() {
let a = 2;
function inner() {
let a = 3;
console.log(a); // 输出:3
}
inner();
}
outer();
2
3
4
5
6
7
8
9
10
11
12
- 上面代码中,查找变量 a 的顺序:
inner()
→outer()
→全局
。所以作用域链就是查找变量的过程。
# 5 window对象
window
对象是 JavaScript 在浏览器环境中最核心的全局对象之一。它代表的是浏览器窗口,我们可以通过 window 对象对浏览器窗口进行各种操作(后面再讲),window 对象负责存储 JS 中的内置对象和函数,例如 String
、Number
、console
、alert()
函数等对象,并充当了 JavaScript 中的全局作用域对象。
window 对象中的属性和函数,可以直接调用,也可以通过 window.
调用。
举个栗子:
alert('Hello');
// 等价于
window.alert('Hello');
console.log('Hello');
// 等价于
window.console.log('Hello');
2
3
4
5
6
7
给 window 对象添加的属性,会自动成为全局变量。
举个栗子:
window.a = 123;
console.log(a); // 123, 全局变量
2
前面讲到 var
声明的变量不具有块作用域,在全局作用域中使用 var
声明的变量,会作为 window 对象的属性。
举个例子:
<script>
var abc = 123;
console.log(abc); // 123
// 等价于
console.log(window.abc); // 123
</script>
2
3
4
5
6
同样,在全局作用域中,使用 function 声明的函数,会作为 window 对象的方法。
举个例子:
function test() {
console.log('Hello');
}
test();
// 等价于
window.test();
2
3
4
5
6
7
看一下下面的情况:
let a = 123;
window.a = 456;
console.log(a); // 123
2
3
- 使用 let 声明的变量是不会作为 window 对象的属性的;
- 直接访问全局变量,会先访问 let 声明的变量;
# 8.7 提升
JavaScript 真的有一些乱七八糟的特性,真的没啥用,开发不用,但面试用。
# 1 变量提升
看一下下面的代码:
不使用 var 声明的变量和使用 var 声明的变量有什么区别?
a = 123;
console.log(a);
var a = 123;
console.log(a);
2
3
4
5
上面的代码有什么区别?用起来好像没区别。
改写一下:
// 先访问a
console.log(a); // 报错:a is not defined
a = 123;
2
3
- 上面的代码
console.log(a)
报错,因为变量 a 没有定义,很好理解,你得先定义后使用;
再看一下 var 声明的变量:
// 先访问a
console.log(a); // undefined
var a = 123;
2
3
- 上面的代码不报错,
console.log(a)
打印变量 a 的值为undefined
。
为什么呢?
这是因为变量提升,使用 var 声明的变量,它会在所有的代码执行前被声明,注意是声明,不是赋值,所以值是 undefined,执行到赋值的代码的时候才被赋值。
可以理解成下面的代码:
var a;
// 先访问a
console.log(a); // undefined
a = 123;
2
3
4
这特性有啥意义,没啥意义,我们在开发中,变量要做到先声明,后使用!
变量在函数中中定义也会被提升的,看一下下面的代码:
var a = 1;
function test() {
console.log(a); // undefined
var a = 2;
console.log(a); // 2
}
test();
console.log(a); // 1
2
3
4
5
6
7
8
9
10
- 在函数内定义的变量 a ,被提升到函数执行之前被声明,所以函数内的第一句
console.log(a);
执行时,函数内的变量 a 没有被赋值,所以是 undefined。 - var 声明的变量没有块作用域但是有函数作用域,所以函数内访问的变量 a 是函数内定义的变量a,不是操作的全局变量a;
- 函数中没有修改全局变量a;
# 2 函数提升
同样使用 function 定义的函数,也会被提升,也就是调用的代码可以写在函数定义前面。
举个栗子:
test(); // 可以写在函数前面
function test() {
console.log('Hello');
}
2
3
4
5
可以理解为下面的代码:
function test() {
console.log('Hello');
}
test(); // 可以写在函数前面
2
3
4
5
需要注意,下面这样是不行的:
func(); // 报错:func is not a function
var func = function () {
console.log("Hello");
};
2
3
4
5
- 上面报错,func 是变量,作为变量被提升了,值为 undefined,肯定不能作为函数来调用;
另外,使用 let 声明的变量也会被提升,但是在赋值之前是禁止被访问的,所以下面这样的代码也会报错:
// 先访问a
console.log(a); // 报错:Cannot access 'a' before initialization
let a = 123;
2
3
- 但是报的错不一样,不是
a is not defined
,而是初始化之前禁止访问。
为什么需要提升?
主要是为了预先分配内存,减少后期重新分配内存的次数,提高执行效率。
# 3 变量同名
使用 let 是不能定义两个相同名称的变量的,let 和 var 也不能定义相同名称的变量。
举个栗子:
let a = 1;
let a = 2; // 报错:Identifier 'a' has already been declared
2
下面的情况也报错:
let a = 1;
var a = 2; // 报错:Identifier 'a' has already been declared
2
但是下面的情况不报错:
var a = 1;
var a = 2;
console.log(a); // 2
2
3
- 后面变量的赋值会覆盖前面变量的赋值。
因为这里涉及到提升,提升后的代码类似:
var a;
a = 1;
a = 2;
console.log(a); // 2
2
3
4
5
所以看一下下面的代码,结构就知道了:
var a = 1;
var a;
console.log(a); // 1
2
3
- 第一句代码,已经提升了变量a,后面重新定义变量a 是没有效果的,因为没有重新赋值。
- 所以后续的声明不会覆盖之前的值,除非显式地重新赋值。
# 4 函数同名
后定义的函数会覆盖前面定义的函数。
举个栗子:
function test(a) {
console.log('Hello')
}
function test(a, b) {
console.log('World')
}
test(); // World
2
3
4
5
6
7
8
9
- 后定义的函数会覆盖前面定义的同名函数。
# 5 变量与函数同名
变量名和函数名可以相同,但需要注意提升和覆盖规则。
举个栗子:
console.log(foo); // 输出:foo() {}
var foo = "Hello";
function foo() {}
console.log(foo); // Hello
2
3
4
- 函数声明会比变量优先提升。
- 如果同名,函数会覆盖变量(除非变量被赋值)。
提升完,代码类似:
function foo() {}
console.log(foo);
var foo = "Hello";
console.log(foo);
2
3
4
5
如果在运行时对同名变量赋值,它会覆盖之前的函数。
function bar() {}
var bar = 1;
console.log(bar); // 输出:1
2
3
但是需要注意,上面都是使用 var 定义的变量,如果使用的 let 或 const,那么变量和函数是没法同名的。
let a = 1;
function a() {} // 报错:Identifier 'a' has already been declared
2
- 我们在开发的时候,要使用 let,不要使用 var。
# 8.8 立即执行函数
在介绍立即执行函数之前,先说一下问题。
我们在实际的开发中,尽量不要在全局作用域中写代码,为什么呢?
因为正常情况下可能多人开发项目,每个人将代码写到各自的 .js
文件中,但是可能在同一个页面中引入,那么这些在全局作用域中定义的变量和函数是会冲突的。
为了解决这个问题,我们可以创建局部作用域,在局部作用域中编写代码。
举个栗子:
{
let a = 1;
console.log(a);
}
{
let a = 2;
console.log(a);
}
2
3
4
5
6
7
8
9
- 上面的代码就创建了两个局部作用域,其中定义的函数和变量就不会冲突了。
但是上面使用的是 let
,如果是 var
就没有块作用域了,那么怎么解决呢?
我们可以使用函数,因为 var 有函数作用域:
function func1() {
var a = 1;
console.log(a);
}
func1(); // 执行才可以
function func2() {
var a = 2;
console.log(a);
}
func2(); // 执行才可以
2
3
4
5
6
7
8
9
10
11
- 上面定义了两个函数,就有了两个函数作用域,两个函数中的变量或函数就不会冲突了。
但是现在仍然有问题,如果在多个文件中定义多个函数,多个人编写没办法保证它们不重名,另外函数需要执行才有效果,所以能否编写一个没有名字(这样就不会重名了)还立即执行的函数呢?
这就来了,我们可以定义只执行一次的匿名函数。
举个栗子:
(function() {
var a = 1;
console.log(a);
})();
2
3
4
- 上面的代码,首先创建一个匿名函数,正常是要赋值给一个变量的,这里没有赋值给变量,所以在外侧添加了
()
。 - 后面的
()
相当于调用函数了,和普通调用函数一样,所以上面的匿名函数会立即执行。
还可以这样写:
(function() {
var a = 1;
console.log(a);
}());
2
3
4
- 上面将调用函数的括号放到括号里面。
这样多个立即执行函数之间就有独立的作用域,不存在冲突了。
(function() {
var a = 1;
console.log(a);
}());
(function() {
var a = 2;
console.log(a);
}());
2
3
4
5
6
7
8
9
10
如果你的代码风格是不加分号 ;
的,那么代码会报错。
你可以在后面添加分号,也可以将分号写在前面,如下:
;(function () {
var a = 1;
console.log(a);
})()
;(function () {
var a = 2;
console.log(a);
})()
2
3
4
5
6
7
8
9
# 8.9 严格模式
JavaScript 有两种代码执行模式:正常模式(Sloppy Mode)和严格模式(Strict Mode)。
它们的主要区别在于严格模式对代码的约束更严格,能帮助开发者避免一些潜在错误,会让代码执行更快,提高代码执行效率。
在开发中能用严格模式,推荐使用严格模式。
# 1 正常模式
代码默认就是正常模式执行,该模式下允许一些不太严谨的语法和行为。
例如不声明变量:
// 正常模式示例
x = 10; // 未声明变量,自动成为全局变量
console.log(x); // 10
2
3
- 正常模式下,代码能不报错就不报错,但可能会导致一些潜在问题。
# 2 严格模式
严格模式通过 "use strict"
指令启用,我们可以在全局开启,也可以只在某个函数中开启。
全局开启严格模式
在 <script>
的标签开始的地方写,或者在整个脚本文件的开始处写。
"use strict";
// x = 10; // 未声明变量,直接使用会报错
let x = 10;
console.log(x); // 10
2
3
4
5
函数开启严格模式
也可以只针对某个函数开启严格模式:
function test() {
"use strict"; // 开启严格模式
// ...严格模式代码
}
test();
2
3
4
5
6
7
# 8.10 this
在函数执行的时候, JavaScript 解释器每次都会传递一个隐含的函数,就是 this
。
# 1 普通函数的this
举个栗子:
function test() {
console.log(this);
}
test();
2
3
4
5
执行如下:
- 可以看出,当调用函数的时候,
this
其实指向的是window
对象。 - 如果是严格模式,输出的是 undefined。
在看一下下面的代码:
function test() {
console.log(this);
}
let person = {
name: 'Doubi',
sayHello: test // 赋值为函数
}
person.sayHello(); // {name: 'Doubi', sayHello: ƒ}
2
3
4
5
6
7
8
9
10
- 此时是通过对象调用方法的形式来调用函数,此时
this
指向的是调用方法的对象。
其实上面两种情况是一样的,因为直接调用函数的时候,也是 window.函数();
,其实是通过 window
对象来调用的,所以 this
是指向调用函数的对象的。
这个有什么用呢?
在使用对象调用函数的时候,可以在函数中,访问到对象的属性。
举个栗子:
let person = {
name: 'Doubi',
sayHello: function() {
console.log(`Hello, I'am ${this.name}`);
}
}
person.sayHello(); // Hello, I'am Doubi
2
3
4
5
6
7
8
- 在上面的代码中,函数中
this
指向的是对象,所以可以通过 this 来访问对象中的属性,此时属性值发生变化,函数是不用修改的。
所以对于普通函数而言,函数中中的 this 和它的调用方式是由关的。
# 2 箭头函数的this
箭头函数没有自己的 this,它的 this 是由外层作用域 this 决定的,所以箭头函数的 this 和它的调用方式无关。
举个例子:
let person = {
name: 'Doubi',
sayHello: () => {
console.log(this);
}
}
person.sayHello(); // window
2
3
4
5
6
7
8
- 上面sayHello 函数中中的 this 为什么指向 window,是因为对象的花括号
{}
不构成作用域,所以箭头函数的 this 会继续向外查找,找到全局作用域,所以指向的是 window。
重新修改一下:
let person = {
name: 'Doubi',
sayHello: function() {
let test = () => {
console.log(this);
}
test();
}
}
person.sayHello(); // {name: 'Doubi', sayHello: ƒ}
2
3
4
5
6
7
8
9
10
11
12
13
- 在上面的代码中,test 箭头函数中的 this 指向的是外层作用域的 this,外层作用域是普通函数,此时外层函数中的 this 指向的是调用的对象,所以内部的箭头函数的 this 指向的也是调用的对象。
# 8.11 对象的简写
ES6 引入了对象简写的语法糖,可以简化对象字面量的定义。
# 1 属性简写
当属性名和变量名相同时,可以省略键值对的 value
,直接写属性名。
举个栗子:
const name = 'Alice';
const age = 25;
// 传统写法
const person = {
name: name,
age: age
};
// 简写写法
const person = {
name, // 属性和变量名相同
age
};
console.log(person); // { name: 'Alice', age: 25 }
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 2 方法简写
对象中的方法可以省略 function
关键字。
举个栗子:
// 传统写法
const person = {
sayHello: function() {
console.log('Hello!');
},
sayGoodbye: function() {
console.log('Goodbye!');
}
};
// 简写写法
const person = {
sayHello() {
console.log("Hello!");
},
sayGoodbye() {
console.log("Goodbye!");
},
};
person.sayHello(); // "Hello!"
person.sayGoodbye(); // "Goodbye!"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24