【函数(下)】

用new操作符调用函数

现在,我们学习一种新的函数调用方式:new 函数()

你可能知道 new 操作符和 “面向对象” 息息相关,但是现在我们先不探讨它的 “面向对象” 意义,而是先把用 new 调用函数的执行步骤和它上下文弄清楚。

用new调用函数的四步走

JS 规定,使用 new 操作符调用函数会进行 “四步走”:

  1. 函数体内会自动创建出一个空白对象
  2. 函数的上下文(this)会指向这个对象
  3. 函数体内的语句会执行
  4. 函数会自动返回上下文对象,即使函数没有 return 语句

四步走详解

1
2
3
4
5
6
7
function fun() {
this.a = 3;
this.b = 5;
}

var obj = new fun();
console.log(obj); // fun { a: 3, b: 5 }

【第一步:函数体内会自动创建出一个空白对象】

1736181971021.png

【第二步:函数的上下文(this)会指向这个对象】

1736181971504.png

【第三步:执行函数体中的语句】

之后这个对象就不再是空对象了。

1736181971985.png

【第四步:函数会自动返回上下文对象,即使函数没有 return 语句】

执行结果为:{a: 3, b: 5}

1736181972564.png

【案例】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fun() {
this.a = 3;
this.b = 6;
var m = 34;
if (this.a > this.b) {
this.c = m;
} else {
this.c = m + 2;
}
}

var obj = new fun();
console.log(obj);

// fun { a: 3, b: 6, c: 36 }

上下文规则总结

规则 上下文
对象.函数() 对象
函数() window
数组[下标]() 数组
IIFE window
定时器 window
DOM事件处理函数 绑定 DOM 的元素
call和apply 任意指定
用new调用函数 秘密创建出的对象

构造函数

1736181973298.png

什么是构造函数

构造函数是专门用来创建对象的函数
一个构造函数我们也可以称为一个类
通过一个构造函数创建的对象,我们称该对象时这个构造函数的实例
通过同一个构造函数创建的对象,我们称为一类对象
构造函数就是一个普通的函数,只是他的调用方式不同,
如果直接调用,它就是一个普通函数
如果使用new来调用,则它就是一个构造函数

我们将之前书写的函数进行一下小改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 书写规范:构造函数首字母大写
// 接收三个参数
function People(name, age, sex) {
// this上绑定三个参数的同名属性
this.name = name;
this.age = age;
this.sex = sex;
}

// 传入三个参数
var xiaoming = new People('小明', 12, '男');
var xiaohong = new People('小红', 10, '女');
var xiaogang = new People('小刚', 13, '男');

console.log(xiaoming); // People { name: '小明', age: 12, sex: '男' }
console.log(xiaohong); // People { name: '小红', age: 10, sex: '女' }
console.log(xiaogang); // People { name: '小刚', age: 13, sex: '男' }
  • 用 new 调用一个函数,这个函数就被称为 “构造函数”,任何函数都可以是构造函数,只需要用 new 调用它
  • 顾名思义,构造函数用来 “构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化
  • 构造函数必须用 new 关键字调用,否则不能正常工作,正因如此,开发者约定构造函数命名时首字母要大写

注意:一个函数是不是构造函数,要看它是否用 new 调用,而至于名称首字母大写,完全是开发者的习惯约定。

如果不用new调用构造函数

1
2
3
4
5
6
7
8
9
10
11
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}

People('小明', 12, '男');
People('小红', 10, '女');
People('小刚', 13, '男');

/* 此时的 this 为 windown 对象,所以下面三条语句会依次给 windown 的三个属性(全局变量)赋值又相互覆盖 */

为对象添加方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
// 添加方法
this.sayHello = function() {
console.log('我是' + this.name + ',我' + this.age + '岁了');
};
}

var xiaoming = new People('小明', 12, '男');
var xiaohong = new People('小红', 10, '女');
var xiaogang = new People('小刚', 13, '男');
xiaoming.sayHello();
xiaohong.sayHello();
xiaogang.sayHello();

var say = xiaoming.sayHello;
say();

/*
我是小明,我12岁了
我是小红,我10岁了
我是小刚,我13岁了
我是undefined,我undefined岁了(上下文为 window)
*/

注意:直接将方法写在构造函数中的方式是不妥的,后面会讲解原因。

类与实例

基本介绍

1736181973756.png

【类好比是 “蓝图”】

如同 “蓝图” 一样,类只描述对象会拥有哪些属性和方法,但是并不具体指明属性的值。

【实例是具体的对象】

1736181974247.png

【构造函数和 “类”】

  • Java、C++ 等是 “面向对象” 语言
  • JavaScript 是 “基于对象” 语言
  • JavaScript 中的构造函数可以类比于 OO 语言中的 “类”,写法的确类似,但和真正 OO语言 还是有本质不同,在后续课程还将看见 JS 和其他 OO 语言完全不同的、特有的原型特性。

JS 构造函数 ≈ OO 语言 “类”

JS 构造函数可以看做是面向对象语言中的 “类”

实例成员和静态成员

1736181974838.png

new出来的实例对象就是构造函数内部的this
实例成员就是this后面的属性和方法

1736181975345.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script type="text/javascript">
function Person(name,age,gender){
this.name = name;
this.age = age;
this.gender = gender;
this.sex = function (){
console.log(this);
};
}
Person.a = 'ds';
Person.sex = function (){
console.log(this);
};
const p1 = new Person('ds',18,'男');
p1.sex();
Person.sex();
</script>

1736181976736.png

  • 实例成员的this指向实例对象
  • 静态成员的this指向该构造函数本身

原型(prototype)

什么是原型

JavaScript 原型是每个对象都具有的属性,它指向一个对象,我们通常称之为原型对象。

当我们访问一个对象的属性或方法时,JavaScript 引擎首先会在该对象自身查找,如果找到则直接使用。如果找不到,则会去该对象的原型对象中查找。

1736181977085.png

任何函数都有 prototype 属性,prototype 是英语 “原型” 的意思。

prototype 属性值是个对象, 每个原型对象里面都有个constructor 属性(constructor 构造函数)
作用:该属性指向该原型对象的构造函数,

constructor:制造商

1736181977944.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function sum(a, b) {
return a + b;
}
console.log(sum.prototype);
console.log(typeof sum.prototype);
console.log(sum.prototype.constructor);
console.log(typeof sum.prototype.constructor);
console.log(sum.prototype.constructor === sum);
/*
{}
object
[Function: sum]
function
true
*/

对于普通函数来说的 prototype 属性没有任何用处,而构造函数的 prototype 属性非常有用

构造函数的 prototype 属性是它的实例的原型

所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法:

  • Date 对象从 Date.prototype 继承。
  • Array 对象从 Array.prototype 继承。
  • Person 对象从 Person.prototype 继承。

所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例

JavaScript 对象有一个指向一个原型对象的链。

创建一个函数以后,解析器都会默认在函数中添加一个数prototype

prototype属性指向的是一个对象,这个对象我们称为原型对象。

构造函数的prototype是实例的原型

实例对象都会有一个属性 proto(对象原型) 指向构造函数的 prototype 原型对象,之所以我们对象可以使用构造函数 prototype 原型对象的属性和方法,就是因为对象有 proto 原型的存在。

1736181978398.png

image-20220813221312823

1
2
这个隐含的属性可以通过对象.__proto__来访问。  
__proto__ 属性:Chrome 提出的一个属性(W3C 中没有)。

原型对象就相当于一个公共的区域,凡是通过同一个构造函数创建的对象他们通常都可以访问到相同的原型对象。

我们可以将对象中共有的属性和方法统一添加到原型对象中,
这样我们只需要添加一次,就可以使所有的对象都可以使用。

1736181978849.png

People.prototypexiaoming 的原型。

1
2
3
4
5
6
7
8
9
10
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex =sex;
}

// 实例化
var xiaoming = new People('小明', 12, '男');
// 测试三角关系是否存在
console.log(xiaoming.__proto__ === People.prototype); // true

补充一张关于原型的图

1736181979469.png

原型链查找

JavaScript 规定:实例可以 “打点” 访问它的原型的属性和方法,这被称为 “原型链查找”。

当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾。

1
2
3
4
5
6
7
8
9
10
11
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
// 在构造函数的 prototype 上添加 nationality 属性
People.prototype.nationality = '中国';

var xiaoming = new People('小明', 12, '男');
// 实例可以 “打点” 访问原型的属性和方法
console.log(xiaoming.nationality); // 中国

1736181980046.png

【遮蔽效应】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex =sex;
}
People.prototype.nationality = '中国';

var xiaoming = new People('小明', 12, '男');
console.log(xiaoming.nationality); // 中国

var tom = new People('汤姆', 10, '男');
tom.nationality = '美国';

// 被遮蔽
console.log(tom.nationality); // 美国

1736181980851.png

hasOwnProperty

hasOwnProperty 方法可以检查对象是否真正 “自己拥有” 某属性或者方法。

1
2
3
4
xiaoming.hasOwnProperty('name');		// true
xiaoming.hasOwnProperty('age'); // true
xiaoming.hasOwnProperty('sex'); // true
xiaoming.hasOwnProperty('nationality'); // false(没有的属性或方法及原型上的属性或方法会返回 false)

in

in 运算符只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法。

1
2
3
4
5
'name' in xiaoming			// true
'age' in xiaoming // true
'sex' in xiaoming // true
'nationality' in xiaoming // true
'love' in xiaoming // false

原型-添加属性和方法

为什么要写在原型上

在之前的课程中,我们把方法都是直接添加到实例身上:

1
2
3
4
5
6
7
8
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex = sex;
this.sayHello = function() { // 方法直接添加到实例身上
console.log('我是' + this.name);
}
}

1736181981665.png

把方法直接添加到实例身上的缺点:每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费。

解决办法:将方法写到 prototype 上。

方法要写到 prototype 上

使用 prototype 属性就可以给对象的构造函数添加新的属性和方法

1736181982551.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function People(name, age, sex) {
this.name = name;
this.age = age;
this.sex =sex;
}
// 方法要写到 prototype 上
People.prototype.sayHello = function() {
console.log('我是' + this.name);
}

People.prototype.sleep = function() {
console.log(this.name + '开始睡觉.zzzz');
}

var xiaoming = new People('小明', 12, '男');
xiaoming.sayHello(); // 我是小明
xiaoming.sleep(); // 小明开始睡觉.zzzz

var tom = new People('汤姆', 10, '男');

// 同一份方法
console.log(xiaoming.sayHello === tom.sayHello); // true

原型对象里面放的是方法, 这个方法里面的this 指向的是这个方法的调用者, 也就是这个实例对象。

原型链

基于原型对象的继承使得不同构造函数的原型对象关联在一起,并且这种关联的关系是一种链状的结构,我们将原型对 象的链状结构关系称为原型链.

Object 可以看做是所有对象的构造函数。

所以,People.prototype 这个对象可以看做是 Object new 出来的。

1736181983400.png

1736181984064.png

1
2
3
4
5
6
7
8
function People() {
}
var xiaoming = new People();

console.log(xiaoming.__proto__.__proto__ === Object.prototype); // true

// Object 是原型链的终点
console.log(Object.prototype.__proto__); // null

查找规则

① 当访问一个对象的属性(包括方法)时,首先查找这个对象自身有没有该属性。
② 如果没有就查找它的原型(也就是 __proto__指向的 prototype 原型对象)
③ 如果还没有就查找原型对象的原型(Object的原型对象)
④ 依此类推一直找到 Object 为止(null)
⑤ __proto__对象原型的意义就在于为实例对象查找机制提供一个方向,或者说一条路线

【关于数组的原型链】

任何数组实际上都是可以看做是 Array 这个构造函数 new 出来的。

1736181984802.png

包装类

定义

  • 在JS中为我们提供了三个包装类:
    String() Boolean() Number()
    通过这三个包装类可以创建基本数据类型的对象

  • 很多编程语言都有 “包装类” 的设计,包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法

  • 例子:

    1
    2
    3
    var num = new Number(2);  
    var str = new String("hello");
    var bool = new Boolean(true);

    NumberStringBoolean这三个原生对象,如果不作为构造函数调用(即调用时不加new),而是作为普通函数调用,常常用于将任意类型的值转为数值、字符串和布尔值。

    总结一下,这三个对象作为构造函数使用(带有new)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new),可以将任意类型的值,转为原始类型的值。

    在实际应用中千万不要这么干。

    某些场合,原始类型的值会自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,并在使用后立刻销毁实例。

    比如,字符串可以调用length属性,返回字符串的长度。

    1
    'abc'.length // 3

    上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var str = 'abc';
    str.length // 3

    // 等同于
    var strObj = new String(str)
    // String {
    // 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"
    // }
    strObj.length // 3

    上面代码中,字符串abc的包装对象提供了多个属性,length只是其中之一。

    自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性。

    1
    2
    3
    var s = 'Hello World';
    s.x = 123;
    s.x // undefined

    上面代码为字符串s添加了一个x属性,结果无效,总是返回undefined

    另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象String.prototype上定义(参见《面向对象编程》章节)。

举例

请看下面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = new Number(123);
var b = new String('慕课网');
var c = new Boolean(true);

console.log(a); // [Number: 123]
console.log(typeof a); // object
console.log(b); // [String: '慕课网']
console.log(typeof b); // object
console.log(c); // [Boolean: true]
console.log(typeof c); // object

console.log(5 + a); // 128
console.log(b.slice(0, 2)); // 慕课
console.log(c && true); // true

var d = 123;
console.log(d.__proto__ == Number.prototype); // true

var e = '慕课网';
console.log(e.__proto__ == String.prototype); // true

实例方法

三种包装对象各自提供了许多实例方法,详见后文。这里介绍两种它们共同具有、从Object对象继承的方法:valueOf()toString()

valueOf()

valueOf()方法返回包装对象实例对应的原始类型的值。

1
2
3
new Number(123).valueOf()  // 123
new String('abc').valueOf() // "abc"
new Boolean(true).valueOf() // true

toString()

toString()方法返回对应的字符串形式。

1
2
3
new Number(123).toString() // "123"
new String('abc').toString() // "abc"
new Boolean(true).toString() // "true"

自定义方法

除了原生的实例方法,包装对象还可以自定义方法和属性,供原始类型的值直接调用。

比如,我们可以新增一个double方法,使得字符串和数字翻倍。

1
2
3
4
5
6
7
8
9
10
11
12
String.prototype.double = function () {
return this.valueOf() + this.valueOf();
};

'abc'.double()
// abcabc

Number.prototype.double = function () {
return this.valueOf() + this.valueOf();
};

(123).double() // 246

上面代码在StringNumber这两个对象的原型上面,分别自定义了一个方法,从而可以在所有实例对象上调用。注意,最后一行的123外面必须要加上圆括号,否则后面的点运算符(.)会被解释成小数点。

总结

  • Number()、String() 和 Boolean() 的实例都是 object 类型,它们的 PrimitiveValue 属性存储它们的本身值
  • new 出来的基本类型值可以正常参与运算
  • 包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法(打点调用)
1
2
3
4
5
var d = 123;
console.log(d.__proto__ == Number.prototype); // true

var e = '慕课网';
console.log(e.__proto__ == String.prototype); // true

从以上代码可以看出,直接定义的基本变量本质也是 new 出来的,所以才可以直接打点调用相关方法。

注意:只有 Number()、String()、Boolean() 才是包装类, 而 Array() 不是数组的包装类,因为数组不是基本类型谈不上 “包装类” 这一说法的。

垃圾回收

  1. 什么是垃圾回收机制?

    垃圾回收机制(Garbage Collection) 简称 GC JS中内存的分配和回收都是自动完成的,内存在不使用的时候会被垃圾回收器自动回收。如果不了解JS的内存管理机制,我们同样非常容易成内存泄漏(内存无法被回收)的情况
    (不再用到的内存,没有及时释放,就叫做内存泄漏)

  2. 内存的生命周期

    JS环境中分配的内存, 一般有如下生命周期:

    1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存

    2. 内存使用:即读写内存,也就是使用变量、函数等

    3. 内存回收:使用完毕,由垃圾回收自动回收不再使用的内存

      说明: 全局变量一般不会回收(关闭页面回收); 一般情况下局部变量的值, 不用了, 会被自动回收掉

1736181985489.png

就像人生活的时间长了会产生垃圾一样,程序运行过程中也会产生垃圾
这些垃圾积攒过多以后,会导致程序运行的速度过慢,
所以我们需要一个垃圾回收的机制,来处理程序运行过程中产生垃圾
当一个对象没有任何的变量或属性对它进行引用,此时我们将永远无法操作该对象,
此时这种对象就是一个垃圾,这种对象过多会占用大量的内存空间,导致程序运行变慢,
所以这种垃圾必须进行清理。
在JS中拥有自动的垃圾回收机制,会自动将这些垃圾对象从内存中销毁,
我们不需要也不能进行垃圾回收的操作
我们需要做的只是要将不再使在使用的变量设置为null

1736181985869.png

高阶函数-回调函数

1736181986364.png

image-20220813143324854

​ 1. 函数表达式 ,函数也是【数据】,把函数赋值给变量

  1. 回调函数 , 把函数当做另外一个函数的参数传递,这个函数就叫回调函数 。回调函数本质还是函数,只不过把它当成参数使用 , 使用匿名函数做为回调函数比较常见。

如果将函数 A 做为参数传递给函数 B 时,我们称函数 A 为回调函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<script>
// 声明 foo 函数
function foo(arg) {
console.log(arg);
}

// 普通的值做为参数
foo(10);
foo('hello world!');
foo(['html', 'css', 'javascript']);

function bar() {
console.log('函数也能当参数...');
}
// 函数也可以做为参数!!!!
foo(bar);
</script>

函数 bar 做参数传给了 foo 函数,bar 就是所谓的回调函数了!!!

我们回顾一下间歇函数 setInterval

1
2
3
4
5
6
7
<script>
function fn() {
console.log('我是回调函数...');
}
// 调用定时器
setInterval(fn, 1000);
</script>

fn 函数做为参数传给了 setInterval ,这便是回调函数的实际应用了,结合刚刚学习的函数表达式上述代码还有另一种更常见写法。

1
2
3
4
5
6
<script>
// 调用定时器,匿名函数做为参数
setInterval(function () {
console.log('我是回调函数...');
}, 1000);
</script>

结论:

  1. 回调函数本质还是函数,只不过把它当成参数使用
  2. 使用匿名函数做为回调函数比较常见

高阶函数-闭包

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 定义一个全局变量
var name = 'ABC';

// 创建一个函数
function fun() {
// 定义局部变量
var name = 'ds';
// 返回一个局部函数
return function() {
console.log(name);
};
}

// 调用外部函数,就能得到内部函数,用变量 inn 来接收
var inn = fun();
// 执行 inn 函数,就相当于在 fun 函数的外部,执行了内部函数
inn(); // "ds"

什么是闭包

1736181986879.png

闭包是函数本身和该函数声明时所处的环境状态的组合。

函数能够 “记忆” 其定义时所处的环境,即使函数不在其定义的环境中被调用,也能访问定义时所处环境的变量。

在 JS 中,每次创建函数时都会创建闭包。

但是,闭包特性往往需要将函数 “换一个地方” 执行,才能被直观的体现出来。

闭包很有用,因为它允许我们将数据与操作该数据的函数关联起来,这与 “面向对象编程” 有少许相似之处。

闭包的功能:记忆性、模拟私有变量(相当于把函数的数据封装了)。

1736181987355.png

闭包常见写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
  简单的写法
function outer() {
let a = 10
function fn() {
console.log(a)
}
fn()
}
outer()


// 常见的闭包的形式 外部可以访问使用 函数内部的变量
function outer() {
let a = 100
function fn() {
console.log(a)
}
return fn
}
// outer() === fn === function fn() {}
const fun = outer();
fun();

// 常见的写法2
function outer() {
let a = 100
return function () {
console.log(a)
}
}
const fun = outer()
fun() // 调用函数


// 常见的写法3(将里面变量的值返回出来)
function outer() {
let a = 100
return function () {
return a
}
}
// console.log(outer())
const fun1 = outer()
// 调用函数
console.log(fun1())

闭包用途 - 记忆性

当闭包产生时,函数所处环境的状态会始终保持在内存中,不会在外层函数调用后自动清除。这就是闭包的记忆性。

【闭包的记忆性举例】

创建体温检测函数 checkTemp(n),可以检查体温 n 是否正常,函数会返回布尔值。

但是,不同的小区有不同的体温检测标准,比如 A 小区体温合格线是 37.1℃,而 B 小区体温合格线是 37.3℃,应该怎么编程呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function createCheckTemp(standardTemp) {
function checkTemp(n) {
if (n <= standardTemp) {
alert('你的体温正常');
} else {
alert('你的体温偏高');
}
}
return checkTemp;
}

// 创建一个 checkTemp 函数,它以 37.1 度为标准线
var checkTemp_A = createCheckTemp(37.1);

// 再创建一个 checkTemp 函数, 它以 37.3 度为标准线
var checkTemp_B = createCheckTemp(37.3);

checkTemp_A(37.2); // "你的体温偏高"
checkTemp_A(37.0); // "你的体温正常"
checkTemp_B(37.2); // "你的体温正常"
checkTemp_B(37.6); // "你的体温偏高"

计数器案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 闭包的应用 
// 普通形式 统计函数调用的次数
// let i = 0
// function fn() {
// i++
// console.log(`函数被调用了${i}次`)
// }
// 因为 i 是全局变量,容易被修改
// 闭包形式 统计函数调用的次数
function count() {
let i = 0
function fn() {
i++
console.log(`函数被调用了${i}次`)
}
return fn
}
const fun = count();

/* 总结:
因为垃圾回收机制,只要还有人使用就不会回收,
用一个全局变量保存使用函数内部变量的函数,
这样那个变量其实一直都在
*/

闭包有点像c语言的静态属性

使用闭包的注意点

不能滥用闭包!否则会造成网页的性能问题,严重时可能导致 “内存泄漏”。

所谓 “内存泄漏” 就是指程序中已经动态分配的内存由于某种原因未释放或无法释放。

目前,Chrome 等比较先进的浏览器很少发生内存泄漏。

闭包面试题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addCount() {
var count = 0;
return function() {
count = count + 1;
console.log(count);
};
}
var fun1 = addCount();
var fun2 = addCount();
fun1(); // 1
fun2(); // 1
fun2(); // 2
fun1(); // 2
/* 即:闭包是独立的 */

立即执行函数 IIFE

形成 IIFE 的方法

IIFE 立即调用函数表达式,是一种特殊的 JS 函数写法,函数定义完,立即被调用,这种函数叫做立即执行函数。
立即执行函数往往只会执行一次

1736181987922.png

蓝色括号里写传递的参数。

1736181988825.png

常用 () 来将函数转为 “函数表达式”。

1
2
3
4
5
6
7
8
9
10
<script type="text/javascript">
/*(function(){
alert("我是一个匿名函数~~~");
})();*/

(function(a,b){
console.log("a = "+a);
console.log("b = "+b);
})(123,456);
</script>

IIFE的作用1 - 为变量赋值

1
2
3
4
5
6
7
8
9
10
11
12
13
var age = 12;
var sex = '男';
var title = (function () {
if (age < 18) {
return '小朋友';
} else {
if (sex == '男') {
return '先生';
} else {
return '女士';
}
}
})();

IIFE的作用2 - 将全局变量变为局部变量

先看一个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var arr = [];
for (var i = 0; i < 5; i++) {
arr.push(function() {
console.log(i);
});
arr[i]();
}
console.log("-------------------")
for (var j = 0; j < arr.length; j++) {
arr[j]();
}

/*
0
1
2
3
4
-------------------
5
5
5
5
5
*/

/*
解释:因为 arr 数组中的每个元素都是 function(){alert(i)} 这个函数,
arr[2] ——> function(){alert(i)}
arr[2](); ——> function(){alert(i)}()
由于 i 为全局变量,所以所有闭包内存都是同一个 i, 而 i 最终确定为 5,根据闭包原理,所以都输出 5;
*/

IIFE 可以在一些场合(如 for 循环中)将全局变量变为局部变量,语法显得紧凑。

在 ES6 中,有更好的方式可以约定变量的作用域

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var arr = [];
for (var i = 0; i < 5; i++) {
(function (i) {
arr.push(function () {
console.log(i);
});
})(i);
}
for (var j = 0; j < arr.length; j++) {
arr[j]();
}

/*
0
1
2
3
4
*/

/*
解释:
function () {console.log(i);},的闭包中的 i 是对应每次循环的 (function (i) {...})(i);
而,function (i) 中的 i 是一个形参,每次的值都是固定的。
*/
函数(上) 数组及常用方法