# JavaScript教程 - 8 函数

什么是函数呢?

先举个栗子:

假设给定两个整数,想获取其中的最大值,那么可以编写代码如下:

// 只是举个栗子,以下代码不是最简洁方式
let num1 = 24;
let num2 = 12;

let maxNum;
if (num1 > num2) {
    maxNum = num1;
} else {
    maxNum = num2;
}

console.log("较大的数为:" + maxNum); 	// 较大的数为:24
1
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, ...) {  // 参数可以省略
  // 实现功能的代码
}
1
2
3

下面定义一个函数,函数实现的功能就是就是打印一行语句:

/**
 * 定义一个函数
 */
function sayHello() {
    alert('Hello, 欢迎来到For技术栈');
}
1
2
3
4
5
6
  • 上面定义了一个函数,函数实现的功能比较简单,就是输出一句话。
  • function 关键字用来定义函数,sayHello 是函数的名字,是自定义的,建议按照标识符命名规则,采用首字母小写的命名方式;
  • 然后是 () ,用来指定参数,这里没有参数,后面会讲,{} 是函数体部分,可以编写函数要实现的功能。

# 2 函数的调用

函数已经定义好了,下面来调用函数:

/**
 * 定义一个函数
 */
function sayHello() {
  alert("Hello, 欢迎来到For技术栈");
}

// 调用函数
sayHello();

// 再次调用函数
sayHello();
1
2
3
4
5
6
7
8
9
10
11
12
  • 调用函数使用 函数名(); 这样的方式来调用,函数可以调用多次,调用一次就是将函数体的代码执行一次。

执行效果如下:


# 3 函数的类型

查看一下函数的类型:

// 定义一个函数
function sayHello() {
  alert("Hello, 欢迎来到For技术栈");
}

console.log(typeof sayHello);  // function
1
2
3
4
5
6
  • 使用 typeof 查看函数类型为 functiontypeof 操作符返回 function 并不是 object ,这只是为了区分函数和普通对象,函数只是对象类型的一种特殊形式。

# 4 匿名函数

上面定义函数的方式指定了函数名称,这种方式为具名函数,还有不指定函数名的函数,称为匿名函数。

举个栗子:

// 定义一个函数
let func = function() {
  alert("Hello, 欢迎来到For技术栈");
}

// 调用函数
func();
1
2
3
4
5
6
7
  • 上面使用 function(){...} 定义了一个匿名函数,并赋值给 func 变量;
  • 在调用的时候,通过变量调用即可。

# 5 箭头函数

ES6 引入的箭头函数,是一种更为简洁的定义函数的方式。

举个栗子:

// 定义一个函数
let func = () => {
  alert("Hello, 欢迎来到For技术栈");
}

// 调用函数
func();
1
2
3
4
5
6
7
  • 上面通过 () => {} 的方式定义函数,并将函数赋值给变量 func,所以箭头函数也是匿名函数。

如果箭头函数的函数体只有一条语句,还可以省略 {}

举个栗子:

// 定义一个箭头函数
let func = () => alert("Hello, 欢迎来到For技术栈");

// 调用函数
func();
1
2
3
4
5

# 8.2 函数的参数

上面定义的函数没有参数,在实际的使用中,函数一般都有参数,例如我们之前使用的 alert('Hello'); 括号中的就是参数,通过传递不同的参数,来让函数针对不同的数据进行处理。

# 1 函数参数

下面来定义一个函数,作用是计算任意两个数的和,所以函数需要接收两个参数,在函数中计算两个参数的和。

代码如下:

/**
 * 定义两个数相加的函数
 */
function add(a, b) {
  let result = a + b;
  console.log("result:", result);
}

// 调用函数
add(1, 2);
1
2
3
4
5
6
7
8
9
10
  • 首先在函数的括号中定义参数,参数个数不限,使用逗号分隔;函数的参数相当于在函数内部定义的局部变量,但是没有初始化。
  • 在调用函数的时候,传递参数,会将值传递给函数的参数进行初始化。在上面的代码中,调用 add() 函数,会将 1 复制给 a2 复制给 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
1
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
1
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
1
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);
}
1
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);
1
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
1
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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行代码,发现 ab 的值并没有被交换,不科学啊,为什么呢。

首先执行:let a = 3; let b = 5; ,函数中的变量会在栈中开辟内存空间:

继续执行,调用 swap 函数,swap 函数的形参 (a, b) 和变量 ab 根本就不是同一个变量,只是这里恰巧名称相同而已,只是将变量a和b的值赋值给了 swap 函数的形参。

然后执行 swap 函数中的代码,创建了 temp 变量,调换了函数中 ab 的值,所以并没有改变变量a和b的值。


再看一段代码:

// 修改a的属性
function change(a) {
  a.name = 'niubi';
}

let a = {name:'doubi', age:18}
change(a);
console.log(a.name);  // niubi
1
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);
1
2
3
4
5
6
7
  • 上面使用 return 将计算的结果返回;
  • 在调用函数时,我们可以使用变量接收返回的值。

我们也可以简化上面的代码:

function add(a, b) {
  return a + b; // 使用 result 返回值
}

console.log(add(2, 3));  // 直接输出调用结果
1
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
1
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
1
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
1
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
1
2
3
4
5
6
7
8
9
  • 函数执行到 return 就返回了,后面的代码无法被执行到。

# 4 箭头函数的返回值

如果箭头函数的函数体只有一句 return 语句,那么可以省略 {}

举个栗子:

let add = (a, b) => a + b;
console.log(add(2, 3));  // 5
1
2

但是这样有一个问题,如果返回的数据是对象类型,编译器没办法区分是函数的 {} 还是 对象的 {}

let obj = (a, b) => {name: name, age: age};  // 无法区分{}是函数的还是对象的。
1

需要使用小括号() ,括起来,如下:

let func = (name, age) => ({name: name, age: age});
let person = func('doubi', 18);
console.log(person.name);  // doubi
1
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();
1
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米
1
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
1
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(不推荐)
1
2
3
4
5
6
  • 非常不推荐

# 2 块级作用域

ES6 引入了 letconst,它们拥有块级作用域。

块级作用域:在一对花括号 {} 中声明的变量,只在这对大括号中有效。

{
    let x = 10;
    const y = 20;
    console.log(x, y); // 正常访问
}
console.log(x); // 报错:x is not defined
1
2
3
4
5
6

var 没有这个能力,会被提升到函数作用域或全局作用域。

if (true) {
  var a = 123;
}

console.log(a); // 输出123,var不受块级作用域限制
1
2
3
4
5
  • 上面的变量a 使用 var 声明,没有块级作用域,因为变量a 也没有在函数中,所以被提升到全局作用域。

# 3 函数作用域

函数内部声明的变量,只能在该函数内部访问。

function test() {
    var a = 123;
    console.log(a); // 正常访问
}

test();
console.log(a);  // 报错:a is not defined
1
2
3
4
5
6
7
  • 上面的变量a 只能在函数内部访问。

因为 var 声明的变量没有块级作用域,所以在函数中声明的变量会被提升为函数作用域(不是块级的)。

function test() {
    if (true) {
        var a = 5;
    }
    console.log(a); // 输出 5,不报错
}

test();
console.log(a);  // 无法访问
1
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();
1
2
3
4
5
6
7
8
9
10
11
12
  • 上面代码中,查找变量 a 的顺序:inner()outer()全局。所以作用域链就是查找变量的过程。

# 5 window对象

window 对象是 JavaScript 在浏览器环境中最核心的全局对象之一。它代表的是浏览器窗口,我们可以通过 window 对象对浏览器窗口进行各种操作(后面再讲),window 对象负责存储 JS 中的内置对象和函数,例如 StringNumberconsolealert()函数等对象,并充当了 JavaScript 中的全局作用域对象

window 对象中的属性和函数,可以直接调用,也可以通过 window. 调用。

举个栗子:

alert('Hello');
// 等价于
window.alert('Hello');

console.log('Hello');
// 等价于
window.console.log('Hello');
1
2
3
4
5
6
7

给 window 对象添加的属性,会自动成为全局变量。

举个栗子:

window.a = 123;
console.log(a);  // 123, 全局变量
1
2

前面讲到 var 声明的变量不具有块作用域,在全局作用域中使用 var 声明的变量,会作为 window 对象的属性。

举个例子:

<script>
  var abc = 123;
  console.log(abc);  // 123
  // 等价于
  console.log(window.abc);  // 123
</script>
1
2
3
4
5
6

同样,在全局作用域中,使用 function 声明的函数,会作为 window 对象的方法。

举个例子:

function test() {
  console.log('Hello');
}

test();
// 等价于
window.test();
1
2
3
4
5
6
7

看一下下面的情况:

let a = 123;
window.a = 456;
console.log(a);  // 123
1
2
3
  • 使用 let 声明的变量是不会作为 window 对象的属性的;
  • 直接访问全局变量,会先访问 let 声明的变量;

# 8.7 提升

JavaScript 真的有一些乱七八糟的特性,真的没啥用,开发不用,但面试用。

# 1 变量提升

看一下下面的代码:

不使用 var 声明的变量和使用 var 声明的变量有什么区别?

a = 123;
console.log(a);

var a = 123;
console.log(a);
1
2
3
4
5

上面的代码有什么区别?用起来好像没区别。

改写一下:

// 先访问a
console.log(a);  // 报错:a is not defined
a = 123;
1
2
3
  • 上面的代码 console.log(a) 报错,因为变量 a 没有定义,很好理解,你得先定义后使用;

再看一下 var 声明的变量:

// 先访问a
console.log(a);  // undefined
var a = 123;
1
2
3
  • 上面的代码不报错,console.log(a) 打印变量 a 的值为 undefined

为什么呢?

这是因为变量提升,使用 var 声明的变量,它会在所有的代码执行前被声明,注意是声明,不是赋值,所以值是 undefined,执行到赋值的代码的时候才被赋值。

可以理解成下面的代码:

var a;
// 先访问a
console.log(a);  // undefined
a = 123;
1
2
3
4

这特性有啥意义,没啥意义,我们在开发中,变量要做到先声明,后使用!


变量在函数中中定义也会被提升的,看一下下面的代码:

var a = 1;

function test() {
  console.log(a);  // undefined
  var a = 2;
  console.log(a);  // 2
}

test();
console.log(a);  // 1
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');
}
1
2
3
4
5

可以理解为下面的代码:

function test() {
  console.log('Hello');
}

test();    // 可以写在函数前面
1
2
3
4
5

需要注意,下面这样是不行的:

func(); // 报错:func is not a function

var func = function () {
  console.log("Hello");
};
1
2
3
4
5
  • 上面报错,func 是变量,作为变量被提升了,值为 undefined,肯定不能作为函数来调用;

另外,使用 let 声明的变量也会被提升,但是在赋值之前是禁止被访问的,所以下面这样的代码也会报错:

// 先访问a
console.log(a);   // 报错:Cannot access 'a' before initialization
let a = 123;
1
2
3
  • 但是报的错不一样,不是 a is not defined ,而是初始化之前禁止访问。

为什么需要提升?

主要是为了预先分配内存,减少后期重新分配内存的次数,提高执行效率。

# 3 变量同名

使用 let 是不能定义两个相同名称的变量的,let 和 var 也不能定义相同名称的变量。

举个栗子:

let a = 1;
let a = 2;  // 报错:Identifier 'a' has already been declared
1
2

下面的情况也报错:

let a = 1;
var a = 2;  // 报错:Identifier 'a' has already been declared
1
2

但是下面的情况不报错:

var a = 1;
var a = 2;
console.log(a);  // 2
1
2
3
  • 后面变量的赋值会覆盖前面变量的赋值。

因为这里涉及到提升,提升后的代码类似:

var a;

a = 1;
a = 2;
console.log(a);  // 2
1
2
3
4
5

所以看一下下面的代码,结构就知道了:

var a = 1;
var a;
console.log(a);  // 1
1
2
3
  • 第一句代码,已经提升了变量a,后面重新定义变量a 是没有效果的,因为没有重新赋值。
  • 所以后续的声明不会覆盖之前的值,除非显式地重新赋值。

# 4 函数同名

后定义的函数会覆盖前面定义的函数。

举个栗子:

function test(a) {
  console.log('Hello')
}

function test(a, b) {
  console.log('World')
}

test();  // World
1
2
3
4
5
6
7
8
9
  • 后定义的函数会覆盖前面定义的同名函数。

# 5 变量与函数同名

变量名和函数名可以相同,但需要注意提升覆盖规则

举个栗子:

console.log(foo);  // 输出:foo() {}
var foo = "Hello";
function foo() {}
console.log(foo);  // Hello
1
2
3
4
  • 函数声明会比变量优先提升
  • 如果同名,函数会覆盖变量(除非变量被赋值)。

提升完,代码类似:

function foo() {}
console.log(foo);

var foo = "Hello";
console.log(foo);
1
2
3
4
5

如果在运行时对同名变量赋值,它会覆盖之前的函数。

function bar() {}
var bar = 1;
console.log(bar); // 输出:1
1
2
3

但是需要注意,上面都是使用 var 定义的变量,如果使用的 let 或 const,那么变量和函数是没法同名的。

let a = 1;
function a() {}   // 报错:Identifier 'a' has already been declared
1
2
  • 我们在开发的时候,要使用 let,不要使用 var。

# 8.8 立即执行函数

在介绍立即执行函数之前,先说一下问题。

我们在实际的开发中,尽量不要在全局作用域中写代码,为什么呢?

因为正常情况下可能多人开发项目,每个人将代码写到各自的 .js 文件中,但是可能在同一个页面中引入,那么这些在全局作用域中定义的变量和函数是会冲突的。

为了解决这个问题,我们可以创建局部作用域,在局部作用域中编写代码。

举个栗子:

{
  let a = 1;
  console.log(a);
}

{
  let a = 2;
  console.log(a);
}
1
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();  // 执行才可以
1
2
3
4
5
6
7
8
9
10
11
  • 上面定义了两个函数,就有了两个函数作用域,两个函数中的变量或函数就不会冲突了。

但是现在仍然有问题,如果在多个文件中定义多个函数,多个人编写没办法保证它们不重名,另外函数需要执行才有效果,所以能否编写一个没有名字(这样就不会重名了)还立即执行的函数呢?

这就来了,我们可以定义只执行一次的匿名函数。

举个栗子:

(function() {
  var a = 1;
  console.log(a);
})();
1
2
3
4
  • 上面的代码,首先创建一个匿名函数,正常是要赋值给一个变量的,这里没有赋值给变量,所以在外侧添加了 ()
  • 后面的 () 相当于调用函数了,和普通调用函数一样,所以上面的匿名函数会立即执行。

还可以这样写:

(function() {
  var a = 1;
  console.log(a);
}());
1
2
3
4
  • 上面将调用函数的括号放到括号里面。

这样多个立即执行函数之间就有独立的作用域,不存在冲突了。

(function() {
  var a = 1;
  console.log(a);
}());


(function() {
  var a = 2;
  console.log(a);
}());
1
2
3
4
5
6
7
8
9
10

如果你的代码风格是不加分号 ; 的,那么代码会报错。

你可以在后面添加分号,也可以将分号写在前面,如下:

;(function () {
  var a = 1;
  console.log(a);
})()

;(function () {
  var a = 2;
  console.log(a);
})()
1
2
3
4
5
6
7
8
9

# 8.9 严格模式

JavaScript 有两种代码执行模式:正常模式(Sloppy Mode)严格模式(Strict Mode)

它们的主要区别在于严格模式对代码的约束更严格,能帮助开发者避免一些潜在错误,会让代码执行更快,提高代码执行效率。

在开发中能用严格模式,推荐使用严格模式。

# 1 正常模式

代码默认就是正常模式执行,该模式下允许一些不太严谨的语法和行为。

例如不声明变量:

// 正常模式示例
x = 10; // 未声明变量,自动成为全局变量
console.log(x); // 10
1
2
3
  • 正常模式下,代码能不报错就不报错,但可能会导致一些潜在问题。

# 2 严格模式

严格模式通过 "use strict" 指令启用,我们可以在全局开启,也可以只在某个函数中开启。

全局开启严格模式

<script> 的标签开始的地方写,或者在整个脚本文件的开始处写。

"use strict";

// x = 10; // 未声明变量,直接使用会报错
let x = 10;
console.log(x); // 10
1
2
3
4
5

函数开启严格模式

也可以只针对某个函数开启严格模式:

function test() {
  "use strict";  // 开启严格模式
  
  // ...严格模式代码
}

test();
1
2
3
4
5
6
7

# 8.10 this

在函数执行的时候, JavaScript 解释器每次都会传递一个隐含的函数,就是 this

# 1 普通函数的this

举个栗子:

function test() {
  console.log(this);
}

test();
1
2
3
4
5

执行如下:

  • 可以看出,当调用函数的时候,this 其实指向的是 window 对象。
  • 如果是严格模式,输出的是 undefined。

在看一下下面的代码:

function test() {
  console.log(this);
}

let person = {
  name: 'Doubi',
  sayHello: test  // 赋值为函数
}

person.sayHello();   // {name: 'Doubi', sayHello: ƒ}
1
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
1
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
1
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: ƒ}
1
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 }
1
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!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24