一、闭包

在 JavaScript 中,我们可以随时创建函数,可以将函数作为参数传递给另一个函数,并在完全不同的代码位置进行调用。我们已经知道函数可以访问其外部的变量。

但如果在函数被创建之后,外部变量发生了变化会怎样?函数会获得新值还是旧值?如果将函数作为参数传递并在代码中的另一个位置调用它,该函数将访问的是新位置的外部变量吗?

通俗的讲:就是函数a的内部函数b,被函数a外部的一个变量引用的时候,就创建了一个闭包。JS中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

作用:能够在函数定义的作用域外,使用函数定义作用域内的局部变量,并且不会污染全局。

原理:基于词法作用域链和垃圾回收机制,通过维持函数作用域的引用,让函数作用域可以在当前作用域外被访问到。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function a(){
var i=0;
function b(){
i++;
alert(i);
}
return b;
}

var c = a();
c();//1
c();//2
c();//3

这个例子中i是函数a中的一个变量,它的值在函数b中被改变,函数b每执行一次,i的值就在原来的基础上加 1 。

因此,函数a中的i变量会一直保存在内存中。

当我们需要在模块中定义一些变量,并希望这些变量一直保存在内存中但又不会 “污染” 全局的变量时,就可以用闭包来定义这个模块。

用处:它的最大用处有两个,一个是它可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

应用

  • 定时器
  • 事件监听器
  • ajax 请求
  • 跨窗口通信
  • web workers
  • 任何其他的异步/同步任务中
  • 只要使用了回调函数,实际上就是使用闭包
  • 实现节流防抖函数

另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let num = new Array();
for(let i=0; i<4; i++){
//闭包被调用了4次,就会生成4个独立的函数
//每个函数内部有自己可以访问的个性化信息
num[i] = f1(i);
}
function f1(n){
function f2(){
alert(n);
}
return f2;
}
num[2]();//2
num[1]();//1
num[0]();//0
num[3]();//3

优点

① 减少全局变量;

② 减少传递函数的参数量;

③ 封装;

缺点

① 使用闭包会占有内存资源,过多的使用闭包会导致内存溢出等

(解决:把那些不需要的变量,但是垃圾回收又收不走的的那些赋值为null,然后让垃圾回收走)

二、原型与原型链

JavaScript 常被描述为一种基于原型的语言——每个对象拥有一个原型对象

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

准确地说,这些属性和方法定义在Object的构造器函数(constructor functions)之上的prototype属性上,而非实例对象本身

原型:每个对象都会在其内部初始化一个属性,就是prototype(原型)。通俗的说,原型就是一个模板,更准确的说是一个对象模板。

原型链:当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去prototype里找这个属性,这个prototype又会有自己的prototype,于是就这样一直找下去,也就是我们平时所说的原型链的概念。

通俗的说,就是利用原型让一个引用类型继承另一个引用类型的属性和方法;比如, Student → Person → Object ,学生继承人类,人类继承对象类。

原型链继承和构造函数继承的例子:

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
function Animal(name) {
this.name = name
}

Animal.prototype.getName = function() {
console.log(this.name)
}

var animal1 = new Animal('Kate')
var animal2 = new Animal('Lucy')

//对象animal1 和 animal2共享方法getName
animal1.getName()
animal2.getName()
//父类:人
function Person () {
this.head = '脑袋瓜子';
this.emotion = ['喜', '怒', '哀', '乐']; //人都有喜怒哀乐
}
//子类:学生,继承了“人”这个类
function Student(studentID) {
this.studentID = studentID;
Person.call(this);
}

var stu1 = new Student(1001);
console.log(stu1.emotion); //['喜', '怒', '哀', '乐']

由这里引申出关于继承的例子(几种常见的):

  • 原型链继承
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function SuperType() {
this.b = [1, 2, 3];
}

function SubType() {}

SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;

var sub1 = new SubType();
var sub2 = new SubType();

// 这里对引用类型的数据进行操作
sub1.b.push(4);

console.log(sub1.b); // [1,2,3,4]
console.log(sub2.b); // [1,2,3,4]
console.log(sub1 instanceof SuperType); // true

  • 构造函数继承
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
function SuperType(name) {
this.name = name;
this.b = [1, 2, 3];
}

SuperType.prototype.say = function () {
console.log("HZFE");
};

function SubType(name) {
SuperType.call(this, name);
}

var sub1 = new SubType();
var sub2 = new SubType();

// 传递参数
var sub3 = new SubType("Hzfe");

sub1.say(); // 使用构造函数继承并没有访问到原型链,say 方法不能调用

console.log(sub3.name); // Hzfe

sub1.b.push(4);

// 解决了原型链继承中子类实例共享父类引用属性的问题
console.log(sub1.b); // [1,2,3,4]
console.log(sub2.b); // [1,2,3]
console.log(sub1 instanceof SuperType); // false

  • ES6 中 class 的继承
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Pet {
    constructor(name, age) {
    this.name = name;
    this.age = age;
    }

    showName() {
    console.log("调用父类的方法");
    console.log(this.name, this.age);
    }
    }

    // 定义一个子类
    class Dog extends Pet {
    constructor(name, age, color) {
    super(name, age); // 通过 super 调用父类的构造方法
    this.color = color;
    }

    showName() {
    console.log("调用子类的方法");
    console.log(this.name, this.age, this.color);
    }
    }

还有组合继承(伪经典继承)、寄生组合式继承 不是很常见,可以自行了解。

做点题练手看看学没学会:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var A=function(){}

A.prototype.n=1

var b=new A()

A.prototype={
n:2,
m:3
}

var c=new A()

console.log(b.n,b.m,c.n,c.m)

答案:1,undefined,2,3

原因是b继承A,所以b.n就为1,而m在A中找不到,所以为undefined

以此类推,c继承的时候A添加了n和m,所以c.n和c.m分别是2和3

其中,undefined是一个表示”无”的原始值,null用来表示尚未存在的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var F=function(){};

Object.prototype.a=function(){
console.log('a()')
};

Function.prototype.b=function(){
console.log('b()')
}

var f=new F();

f.a()//?
f.b()//?
F.a()//?
F.b()//?

答案:a()、报错找不到b这个函数、a()、b()。

F 是个构造函数,而 F 是构造函数 Function 的一个实例。因为 F instanceof Object === true 、F instanceof Function === true,由此我们可以得出结论:F 是 Object 和 Function 两个的实例,即 F 能访问到 a, 也能访问到 b。

对于 f ,f 并不是 Function 的实例,因为它本来就不是构造函数,调用的是 Function 原型链上的相关属性和方法了,只能访问到 Object 原型链。所以 f.a() 输出正常,而 f.b() 就报错

F.a 的查找路径:F 自身:没有 —> F.__ proto __ (Function.prototype):没有—> F.__ proto __ . __ proto __(Object.prototype):找到了输出 a()

F.b 的查找路径:F 自身:没有 —> F.prototype(Function.prototype):b()

f.a 的查找路径:f 自身:没有 —> f. __ proto __ (Object.prototype):输出 a()

f.b 的查找路径:f 自身:没有 —> f. __ proto__ (Object.prototype):没有 —> f. __ proto __ . __ proto__ (Object.prototype.__ proto__:null):找不到,报错

参考

【1】https://zhuanlan.zhihu.com/p/129022735

【2】原生的原型 (javascript.info)

【3】https://febook.hzfe.org/awesome-interview/book2/js-inherite