原型

  • 原型的出现,就是为了解决 构造函数的缺点
  • 也就是给我们提供了一个给对象添加函数的方法

prototype 显示原型

  • 每个函数天生自带的一个属性,叫 prototype,是一个对象空间
  • 既然每个函数都有,构造函数也是函数,构造函数也有这个对象空间
  • 这个prototype的对象空间可以由函数名来访问
1
2
function Person() {}
console.log(Person.prototype); //是一个对象
  • 是对象,我们就可以往里面放东西
1
2
3
4
5
6
7
8
9
10
11
// let obj = { }
// obj.name = "xxx"
// obj.sayHi = function(){}
function Person() {}
Person.prototype.name = 'prototype';
Person.prototype.sayHi = function () {};
console.log(Person.prototype); //有属性有方法
console.log(Person.sayHi); //undefined 访问不到

let p1 = new Person();
console.log(p1.name, p1.sayHi); //能访问到
  • 我们发现了一个叫做prototype的空间和函数有关
  • 并且我们可以向里面放一些东西
  • 重点:在函数的 prototype 里面存储的内容,不是给函数本身使用的,是给函数的每一个实例化对象使用的

__proto__ 隐式原型

  • 每个对象天生自带的一个属性,叫做__proto__,也是一个对象空间
  • 既然每个对象都有,实例化对象也是对象,每一个实例化的对象都有这个属性
  • 这个__proto__对象空间是给对象使用的
  • 当你访问对象中的一个属性的时候
    • 如果这个对象本身有这个属性,那么直接给你结果
    • 如果没有,就会去__proto__这个对象空间里面找,有的话就给你结果
    • 未完待续
  • 那么这个__proto__指向哪里?
    这个对象由那个构造函数 new 出来
    那么这个对象的__proto__就指向该构造函数的prototype
1
2
3
4
function Person() {}
let p = new Person();
//p.__proto__和Person.prototype 就是一个空间
console.log(p.__proto__ === Person.prototype); //true
  • 实例化对象的隐式原型和构造函数的显示原型是同一个空间
  • 我们就可以通过构造函数的prototype来给实例化对象添加属性
  • 对象访问属性的时候,可以去自己的__proto__中查找
  • 之前的构造函数不合理的地方就解决了
    • 我们把对象的方法放在构造函数的prototype
    • 实例化的对象访问的时候就去自己的__proto__
    • 也可以使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Person() {}
Person.prototype.sayHi = function () {
console.log('你好');
};
let p = new Person();
let p1 = new Person();
p.sayHi();
p1.sayHi();
/*
p本身没有sayHi方法,就去自己的__proto上找
p.__proto__就是Person.prototype
我们在上一步 给Person.prototype添加了一个sayHi方法
p.sayHi()肯定能执行
*/

p 和 p1 使用的实际上就是同一个函数

prototype__proto__ 有什么区别

  1. 名字不一样
  2. 有可能指向同一个对象空间
    => prototype 前面主语一定是函数
    => proto 前面的主语一定是对象

小结

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
/* 
原型:
显示 prototype
每一个函数天生自带,是一个对象数据类型
构造函数也是函数,所以也有prototype
里面有一个constructor的属性 1. 表明我是谁自带的prototype 2.指向自己的构造函数
是一个对象,我们可以往里面添加属性

隐式 __proto__
每个对象天生自带,也是一个对象数据类型
实例对象也是对象 所以也有__proto__

对象属性的访问规则
1. 自己有就给你
2. 自己没有就去__proto__上找 有给你
没有??
3、自己有,实例对象的原型上也有,但不会访问原型上的这种情况被称为"属性遮蔽(property sha)"

以后写构造函数的时候
属性直接写在函数体内
方法写在构造函数的显示原型上(挂载)
*/
function Person() {
this.name = 'alex';
}
//给Person的原型上添加了一个属性
Person.prototype.a = 100;
//p.__proto__.a=100
let p = new Person();
console.log(p.a); //100

面向对象的开发思想

核心:高内聚低耦合

意义:

  • 在开发的时候
  • 首先写一个构造函数
  • 这个构造函数可以生成能够完成对应的功能对象

原型和原型链

  • 原型:每个函数天生自带 prototype 对象数据类型

    • 作用:由构造函数 向原型上添加方法,给该构造函数的所有实例对象用
    • 为了解决将方法写在构造函数体内造成的资源浪费
  • 原型链:

    • __proto__串联起来的对象链状结构
    • 作用:为了对象的访问机制服务的
    • 注意:只是__proto__串联起来的对象,千万不要往 prototype 靠

万物皆对象

问题

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
46
47
48
49
50
51
52
53
function Person(nam, age, gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
Person.prototype.sayHi = function () {
console.log('hahah');
};
const p1 = new Person('alex', 18, '男');
console.log(p1);
//问题一:p1的__proto__是谁?
console.log(p1.__proto__ === Person.prototype); //true
//2. Person的__proto__是谁?
/*
Person是构造函数,同时是一个函数 同时也是一个对象
只要是对象就会有__proto__属性
js内有一个内置的构造函数叫做Function 只要是函数 都可以看作Function的实例
*/
console.log(Person.__proto__ === Function.prototype);

//3. Person.prototype.__proto__是谁?
/*
Person的prototype是函数天生自带的一个对象数据类型
只要是对象就会有__proto__
js内有一个内置的构造函数 叫做 Object 只要是对象 都可以看作Object的实例
Person.prototype对象数据类型 是对象 可以看作Object的实例
*/
console.log(Person.prototype.__proto__ === Object.prototype);

//4. Function 的__proto__?
//Function是一个构造函数,是函数也是对象
//Function自己是自己的实例,自己是自己的构造函数
//js中 他叫做顶级函数
console.log(Function.__proto__ === Function.prototype);
//5. Function.prototype.__proto__ ?
/*
Function.prototype 是函数天生自带一个对象数据类型
是对象就有__proto__
把Function.prototype看作Object的实例
*/
console.log(Function.prototype.__proto__ === Object.prototype);
//6. Object.__proto__?
//Object是构造函数 是对象也是函数
// 只要是对象就有__proto__
// 是构造函数 是函数 是函数 看作Function的实例
console.log(Object.__proto__ === Function.prototype);
//7.Object.prototype.__proto__?
//Object.prototype是函数天生自带的一个对象数据类型
//是对象 就有__proto__
//在js Object顶级对象 Object.prototype叫做顶级原型
//Object.prototype唯一一个没有__proto__的数据类型
// null
console.log(Object.prototype.__proto__); //null

对象访问机制

当你访问一个对象的属性的时候
首先在自己身上查找,如果有,给你 停止查找
如果没有,__proto__自己去上找
如果还没有,再去__proto__上 找
依此类推,直到 顶级原型上(Object.prototype)都没有,返回 undefined

结论:
Object.prototype 添加的内容,所有的对象数据类型都可以使用
Function.prototype 添加的内容,所有的函数数据类型都可以使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function Person() {
this.name = 'alex';
this.age = 18;
this.gender = '男';
}
Person.prototype.a = function () {
console.log('Person.prototype');
};
Object.prototype.b = function () {
console.log('Object.prototype');
};
Function.prototype.c = function () {
console.log('Function.prototype');
};
const p = new Person();
p.a();
p.b();
// p.c(); 报错
// Person.a();报错
Person.b();
Person.c();

例题

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 getName() {
console.log(5);
}
var getName;
function Father() {
getName = function () {
console.log(1);
};
return this;
}
Father.getName = function () {
console.log(2);
};
Father.prototype.getName = function () {
console.log(3);
};
getName = function () {
console.log(4);
};

//对象中的方法
Father.getName(); //2
//全局的getName方法
getName(); //4
new Father.getName(); //2 {}
//Father的实例去找getName
new Father().getName(); //3 undefined
//new Father()=>实例对象
// new 实例对象.getName()
new new Father().getName(); //3 {}

继承

1. 认识继承

关于 构造函数 的高阶应用
继承是出现在两个构造函数之间的关系

当构造函数 B 的实例,使用了构造函数 A 内书写的属性和方法
此时我们就说 构造函数 B 继承自 构造函数 A
构造函数 A 是构造函数 B 的父类
构造函数 B 是构造函数 A 的子类

  1. 原型继承
  2. call 继承
  3. 组合继承
  4. es6 的继承(完美)

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
33
34
35
36
37
38
//父类
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log('hello world');
};
//子类
function Student(gender) {
this.gender = gender;
}
//当Student构造函数书写完毕后
//此时 会伴生出一个Student.prototype的对象数据类型
//我们可以向Student.prototype添加属性
// Student.prototype.a=100
// 我们也可以重新赋值
//本身你的Student.prototype保存的是一个 对象数据类型的 地址
//我 给你赋值为一个 新的 对象数据类型 的 地址
//就把你本身保存的地址覆盖掉
// const obj = {
// msg: 'objjjjjj',
// };
// //验证覆盖
// Student.prototype = obj;

//实现继承
Student.prototype = new Person('alex', 18);

// Student.prototype = obj;
Student.prototype.play = function () {
console.log('没日没夜的玩');
};
//目前 s只有play属性和gender属性
//继承就是要s有name和age 还有sayHi
const s = new Student('男');
// console.log(s.name, s.age, s.sayHi);
console.log(s);
  • 核心:子类原型指向父类的 实例
  • 子类.prototype = new 父类

优缺点
优点:父类的 构造函数体内 和 原型上的所有内容都能继承

缺点:继承下来的属性不在自己身上,子类的实例的所有属性分开了两部分书写,s 自身有一部份,原型上面有一部份。同样是给 子类的实例 使用的属性,在两个位置传递参数 new Person()传递一部份,new Student() 传递一部分

不是不可以,只是不够友好

3. call 继承

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 Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log('hello world');
};
/*
call继承
+ 核心 利用call调用父类的构造函数
*/
//子类
function Student(gender, ...arg) {
this.gender = gender;
//this 只要你new 永远是实例对象
//实现继承
Person.call(this, ...arg);
}
// function Student(gender, name, age) {
// this.gender = gender;
// //this 只要你new 永远是实例对象
// //实现继承
// Person.call(this, name, age);
// }
Student.prototype.play = function () {
console.log('没日没夜的玩');
};
const s = new Student('男', 'alex', 18);
// Person.call(s, 'alex', 18);
console.log(s);
/*
构造函数
构造函数本身也是一个函数
是函数就能直接调用,可以不和new连用
只是没有new的话,就没有创建对象的能力
*/
//把Person当作一个普通函数来调用,Person内的this->window
//Person函数体内的两个属性 加给了window
// Person('haha', 18);不报错
//既然Person能够当作普通函数来调用
//call来调用函数并且改变里面的this指向
// const obj = { msg: '我是obj' };
// Person.call(obj, 'tom', 28);
// console.log(obj);

核心:利用 call 调用父类的构造函数

优缺点:
优点:可以把继承来的属性直接出现在 子类的实例 身上,一个实例需要用的属性可以在同一个位置传递参数
缺点:只能继承 构造函数体内书写的内容, 构造函数 原型上的 不能继承

4. 组合继承

原型继承和 call/apply 继承 优缺点刚好互补
一种新的相对完美的继承方式就出来了,利用好这两种方式的优缺点

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 Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log('hello world');
};
/*
组合继承:
核心:把 原型继承 和 call继承 合在一起
*/
//子类
function Student(gender, ...arg) {
this.gender = gender;
//实现call继承,
//目的:把构造函数体内的属性都拿到
Person.call(this, ...arg);
}
//实现原型继承
//目的:拿到Person.prototype上的方法
//这里new Person()可以不用写,因为对象属性的访问机制
// Student.prototype = new Person('jack', 18);
Student.prototype = new Person();
Student.prototype.play = function () {
console.log('没日没夜的玩');
};
let s = new Student('男', 'alex', 88);
console.log(s);
console.log(s.name, s.age); //alex 88

5. es6 继承

  1. 使用 extends 关键字
    class 子类 extends 父类{…}
  2. 在子类的 constructor 内书写
    super()

注意:必须两个条件同时书写,super 必须写在所有的 this 之前,父类如果是一个 es5 的构造函数,那么也可以正常继承

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
//父类
// class Person {
// constructor(name, age) {
// this.name = name;
// this.age = age;
// }
// sayHi() {
// console.log('hello world');
// }
// }

//es5的
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHi = function () {
console.log('hello world');
};
//子类
class Student extends Person {
constructor(gender, name, age) {
//super就类似于call调用父类构造函数
//把name和age传递过去
super(name, age);
this.gender = gender;
}
play() {
console.log('没日没夜的玩');
}
}
let s = new Student('男', 'alex', 18);
console.log(s);

js的运行原理

V8如何把js运行起来

V8引擎原理

js代码的执行

高级语言写的代码最终都是通过计算机硬件(CPU)运行的,

但计算机不能直接识别js的代码,需要通过一定的转换,变成计算机能够识别的二进制机器码,计算机执行后才能看到实现的效果。

什么是V8

V8是Google使用C++编写的开源高性能JS和WebAssmbly引擎,广泛应用于chrome和Node.js,能够跨平台,独立运行,还能嵌入任何C++应用程序中

V8架构

Parse 解析(https://v8.dev/blog/scanner)

  • 将js代码转化成AST(抽象语法树),如果函数没有调用,不会将函数转化成抽象树。

Ignition 解释器(https://v8.dev/blog/ignition-interpreter)

  • 将AST转换成bytecode(字节码)
  • 帮助Turbo Fan收集信息
  • 如果函数只调用一次,Ignition会执行bytecode

Turbo Fan 编译器(https://v8.dev/blog/turbofan-jit)

  • 收集信息,优化代码的执行速度
  • 反向优化,重新将机器码转化成字节码

多次调用的函数在Turbo Fan中也叫热点函数

Garbage Collection 垃圾回收机制

js执行上下文GO AO VO

2.1 初始化全局对象

  • 在执行代码之前,js引擎会在堆内存中创建一个全局对象:Global Object(GO)
  • 在浏览器中全局对象就是window,所以在执行所有代码之前window已经创建好了,该对象所有的作用域都能访问,里面会包含Date、Math、String

2.2 执行上下文

js引擎内部有一个执行上下文,它是用于执行代码的调用栈

GEC(全局执行上下文)被放入ESC(执行上下文)中包含两部分内容

  1. 在代码执行之前,在parser转成AST的过程中,它会将全局定义的变量,函数放入到GO中,但是不会赋值,这个过程也被称为变量作用域的提升
  2. 在代码执行过程中,对变量赋值或者执行其他的函数

只要执行代码js就会创建一个执行上下文

2.3 VO对象

  • 每一个执行上下文关联一个对应的VO(variable Object)变量和函数都会被添加到VO对象中

  • 对于全局代码而言我们的VO就是GO,但是函数有点特殊,它会优先把函数对象创建出来,创建的时候里面默认有name,length,arguments

补充:Function.length属性

length 属性指明函数的形参个数

length 是函数对象的一个属性值,指该函数期望传入的参数数量,即形参的个数。

形参的数量不包括剩余参数(…args)个数,仅包括第一个具有默认值之前的参数个数。

与之对比的是,arguments.length 是函数被调用时实际传参的个数。

3.函数代码执行

总结:

当我们去执行一段代码,不管是全局代码还是函数内部的代码,都会创建一个对应的执行上下文,如果执行的是全局代码,创建的VO就是GO,如果执行的是函数内部的代码,创建的VO就是AO

全局代码如何执行

函数的 length 属性
  1. 形参个数
  • 函数全部的形参没有默认值,则function的length就是形参的个数
1
2
3
4
5
6
function fn1() {}
function fn2(name) {}
function fn3(name, age) {}
console.log(fn1.length); //0
console.log(fn2.length); //1
console.log(fn3.length); //2
  1. 默认参数
  • 如果函数的形参有默认值,则functionlength第一个具有默认值之前的形参个数
1
2
3
4
5
6
7
8
9
10
11
12
13
function fn1(name) {}
function fn2(name = 'alex') {}
function fn3(name, age = 22) {}
function fn4(name, age = 22, gender) {}
function fn5(name, age, gender = '男') {}
function fn6(name = 'alex', age, gender) {}

console.log(fn1.length); //1
console.log(fn2.length); //0
console.log(fn3.length); //1
console.log(fn4.length); //1
console.log(fn5.length); //2
console.log(fn6.length); //0
  1. 剩余参数
  • 剩余参数不计入函数的长度中
1
2
function fn1(name, ...args) {}
console.log(fn1.length); //1

面试题:123['toString'].length+123=124

3.函数代码执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var message = 'Global Message';

function foo(num) {
var message = 'Foo Message';
var age = 18;
var height = 1.88;
console.log('foo function');
}

foo(123);

var num1 = 10;
var num2 = 20;
var result = num1 + num2;
console.log(result);
  • 因为每个执行上下文都会关联一个 VO,那么函数执行的时候也要关联对应的 VO,函数的 VO 叫做 AO 对象(Activation Object),这个 AO 对象会作为执行上下文的 VO 来存放函数中的私有变量

易错点:创建函数的时候就会创建一个 AO 对象,完全不对,这两者没有任何关系,AO 是函数调用的时候才有的

4.函数代码多次执行

重新创建对应的 EC,关联对应的 VO,创建对应的 AO,执行完以后弹出栈,如果我们不做任何其他的操作,销毁 AO。

5. 函数代码互相调用

一样的操作,只不过是创建对应的 EC,关联对应的 VO,创建对应的 AO,执行完以后弹出栈,如果我们不做任何其他的操作,销毁 AO

函数执行过程

作用域和作用域链

6.1 全局变量的查找顺序

去自己的 VO 的查找

1
2
3
console.log(message);
var message = 'Global message';
console.log(message);

作用域提升:对后面定义的变量做一个提前的访问。

6.2 函数变量的查找顺序

1
2
3
4
5
6
7
8
9
console.log(message);
var message = 'Global message';

function foo() {
var message = 'foo message';
console.log(message);
}

foo();
作用域链

作用域链它是一个对象列表,用于变量标识符的求值
当进入一个执行上下文的时候,作用域链被创建,并且根据代码类型,添加一系列的对象。

如果是全局,作用域链就只有一个 GO;如果是函数,当函数被创建的时候,而不是调用,它的作用域链就被确定了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
console.log(message);
var message = 'Global message';

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

foo();

var obj = {
name: 'obj',
messsage: '12312',
bar: function () {
var message = 'obj内的bar';
foo();
},
};

obj.bar();
obj['bar']();

6.3 多层嵌套的查找顺序

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log(message);
var message = 'Global message';

function foo() {
var name = 'foo';
function bar() {
console.log(name);
}
return bar;
}

var bar = foo();
bar();

6.4 几道经典的面试题

1
2
3
4
5
6
var n = 100;
function foo() {
n = 200;
}
foo();
console.log(n); //200

foo 自身的 AO 里面没有 n,去找[[Scopes]],只有一个 GO,修改成 200


1
2
3
4
5
6
7
var n = 100;
function foo() {
console.log(n);
var n = 200;
console.log(n);
}
foo();

1
2
3
4
5
6
7
8
9
10
var n = 100;
function foo1() {
console.log(n);
}
function foo2() {
var n = 200;
console.log(n);
foo1();
}
foo2();

1
2
3
4
5
6
7
var n = 100;
function foo() {
console.log(n);
return;
var n = 200;
}
foo();

1
2
3
4
5
6
//开发之中很可能出现的错误❌写法
function foo() {
msg = 'hello world';
}
foo();
console.log(msg);

1
2
3
4
5
6
function foo() {
var a = (b = 200);
}
foo();
console.log(a); //报错
console.log(b);

javascript 的内存管理

1.1 认识内存管理

不管是什么样的编程语言,在代码的执行过程中都是需要给他分配内存的,不同的是有些编程语言需要我们自己手动管理内存,有些编程语言可以自动管理内存。

不管是什么方式来管理内存,内存的管理都会有如下的生命周期:

  1. 分配申请你需要的内存(申请)
  2. 使用分配的内存
  3. 不需要使用时,对其进行释放

1.2 js 的内存管理

  • 对于原始数据类型,直接在栈里面分配

  • 对于复杂数据类型,在堆里面分配,并将这块空间的指针返回给你的变量名引用

2. 垃圾回收机制算法

2.1 js 的垃圾回收(Garbage Collection)

原因:因为内存的大小是有限的,所以当内存里面的对象不再需要时,我们就要对他进行释放,以便腾出空间。
Garbage Collection,简称 GC。

2.2引用计数(Reference counting)

  • 当一个对象有一个引用指向他时,这个对象的引用就+1;
  • 当一个对象的引用为 0 时,这个对象就可以被销毁
1
2
3
4
5
let user = { username: 'alex chen' }; //对象被引用 计数+1
let user2 = user; //对象又被引用 计数+1
user = null; //变量不再引用对象 计数-1
user2 = null; //变量不再引用对象 计数-1
//此时 这个对象 计数为0 可以销毁

这个方法虽然看起来合理,但是存在明显的漏洞,那就是循环引用

1
2
3
4
5
6
7
8
9
let boy = {};
let girl = {};
boy.girlfriend = girl;
girl.boyfriend = boy;
boy = null;
girl = null;
//想要清除
boy.girlfriend = null;
girl.boyfriend = null;

2.3 标记-清除(mark-Sweep)

核心思路是可达性,这个算法是设置一个根对象(Root Object),垃圾回收器会定期从根开始,找到所有从根开始引用的对象,对于那些没有的对象,认为是后续不再用的,就给他销毁,这个算法可以很好的解决循环引用的问题。

2.4 常见的其他 GC 算法

  1. 标记整理(Mark-Compact)
  2. 分代收集(Generational collection)
    对象被划分成两组:’新的’和’旧的’
    减少对老旧对象的扫描频率
  3. 增量收集(Incremental collection) 大的->很多个小的
  4. 闲时收集(idle-time collection)

2.5 V8 引擎详细内存图

3. 闭包

3.1 js 的函数式编程

js 中函数非常重要,而且是一等公民。函数可以作为另一个函数的参数,也可以作为函数的返回值。
vue:optionsAPI->compositionAPI->函数 hook
react:class->function->hooks

3.2 闭包的概念

在计算机科学:Closure,又叫词法闭包(Lexical Closure)或者函数闭包,是在头等函数的编程语言中,实现词法绑定的一种技术,闭包在实现上是一个结构体,他存储了一个函数和一个关联的环境。闭包跟函数最大的区别在于,当捕获闭包的时候,他的自由变量会在被捕捉时确定。这样即使脱离了上下文,他也能照常运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var name = 'alex';
var age = 18;
var info = '唱跳rap';
//有闭包
function foo() {
var msg = 'hello world';
//js底层帮你做了一件事--闭包
console.log(msg, name, age, info);
}
//没有闭包
function bar(name, age, info, address, hobby, height) {
var msg = 'hello world';
//js底层帮你做了一件事--闭包
console.log(msg, name, age, info);
function baz() {
console.log(msg, name, age, info);
}
}
//这里 既有外面的变量 也有我自己的
//全部混淆在一起 --- 希望一个函数只做一件事
bar(name, age, info, address, hobby, height);
foo();
//作用域链的存在,就是为了方便你去外面找东西,也就是为了闭包

私人:
广义上来说,js 中的函数都是闭包。
狭义说:js 中的函数,访问了外层作用域的变量,才叫做闭包。

4. 闭包的形成过程

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
//新手
// function adder(num,count) {
// return num + count;
// }
// adder(100,5); //105
// adder(5,5); //10
// adder(55,5); //60
// //我要 后三次调用+8
// adder(22,8)
// adder(12,8)
// adder(2,8)
//nest.js 全栈
//老手
function createAdder(count) {
// function adder(num) {
// return count + num;
// }
// return adder;
return function (num) {
return count + num;
};
}
// let createAdder1 = count=> num => count + num;
// let adder4= createAdder1(4)
// let res = adder4(16)
// console.log(res);
let adder5 = createAdder(5);
adder5(15); //20
adder5(25);
adder5(35);
let adder8 = createAdder(8);
adder8(2);
adder8(12);
adder8(22);

看图

5. 闭包的内存泄漏

我们图里 adder5 adder8 以及对应的 AO 对象,存在引用而且也是根可达的所以即使后续不再使用也不会释放。我们经常说的闭包的内存泄漏,其实就是这些引用链上的对象无法释放。

5.1 如何释放

比如说我们要释放 adder5,adder5=null

5.2 内存泄漏的测试

2023-05-16-11-56-33.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
<button class="create">创建很多对象</button>
<button class="destory">销毁很多对象</button>
<script>
function createArray() {
//4 1024 ->4kb *1024 ->4M
var arr = new Array(1024 * 1024).fill(1);
function test() {
console.log(arr);
}
return test;
}
var totalArr = [];
let createBtn = document.querySelector('.create');
let destoryBtn = document.querySelector('.destory');
createBtn.onclick = function () {
for (let i = 0; i < 100; i++) {
totalArr.push(createArray());
}
console.log(totalArr.length);
};
destoryBtn.onclick = function () {
totalArr = [];
};
</script>

5.3 AO 不使用的属性优化

不被销毁的 AO 对象,里面所有的属性都一直保留吗?
结论:不使用的不会保留–浏览器自己的优化

1
2
3
4
5
6
7
8
9
10
11
12
function foo() {
var name = 'foo';
var age = 18;
var height = 1.88;
function bar() {
debugger;
console.log(name);
}
return bar;
}
var fn = foo();
fn();

函数的增强知识

1.1 函数的属性和 arguments

1.1.1 函数的属性

1
2
3
4
5
function foo(a, b, c) {}
let obj = {};
obj.address = '天河';
//自定义属性
foo.msg = 'hello foo';
  • 除了自定义属性以外,函数对象中已经有一些自己的属性。
  1. name:函数的名字
  2. length:第一个具有默认值参数之前的参数的个数

1.1.2 Arguments类数组对象

概念:**arguments** 是一个对应于传递给函数的参数的类数组对象。

  • array-like 对象不是一个真正的数组,它类似于Array,但除了 length 属性和索引元素之外没有任何Array属性。如pop、filter、map等方法
  • 在开发中,如果需要对 arguments 进行操作的时候要用到数组的一些特性,那我们就需要把他转成数组。

转成数组的三种方法

  1. 遍历添加
1
2
3
4
5
6
7
8
9
function foo() {
console.log(arguments);
let newArr = [];
for (const arg of arguments) {
newArr.push(arg);
}
console.log(newArr);
}
foo(10, 20, 12, 11, 333);
  1. es6 的方法
1
2
let newArr = [...arguments];
let newArr1 = Array.from(arguments);
  1. slice+apply
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//回顾slice
let arr = ['a', 'b', 'c'];
//浅拷贝,引用赋值
let bar = arr.slice(); //this->arr

//对象方法的调用
// obj.foo() //this->obj
console.log(arr, bar);
function foo() {
console.log(arguments);
//把arguments拷贝一份
// slice.apply({1,2,3,,3213});
// slice.apply(arguments);
let newArr = Array.prototype.slice.apply(arguments);
let newArr1 = [].slice.apply(arguments);
}
//找方法把arguemnts转成数组
//slice返回一个数组给你
//找slice
foo(10, 20, 12, 11, 333);
箭头函数的 arguments
  • 箭头函数中没有arguments
1
2
3
4
5
6
7
8
9
function foo() {
//拿到的是外层作用域的arguments
let bar = () => {
//自己没有arguements
console.log(arguments);
};
bar();
}
foo(11, 22);
ES6的Rest parameters(剩余参数)

概念:将不定量的参数放入一个数组中

1
2
3
4
function foo(num1, num2, ...otherNums) {
console.log(otherNums);
}
foo(1, 2, 3, 4, 5, 6, 7);

剩余参数和 arguments 区别

  1. 剩余参数是一个真正的数组,arguments 是一个类数组对象
  2. 剩余参数只包括没有对应形参的实参,arguments 包括所有的实参。
  3. arguments对象还有一些附加的属性(如callee属性)

注意:剩余参数只能写在最后面

1.2 纯函数的理解和应用

1.2.1 纯函数的理解

在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数

  1. 此函数在相同的输入时,需产生相同的输出
  2. 函数的输出和输入值和其他的隐藏信息或者外部设备状态无关
  3. 该函数不能有语义上可观察的函数副作用

私人:

  1. 确定的输入,一定产生确定的输出
  2. 函数执行的过程中,不能产生副作用

计算机中的副作用

  • 概念:在执行一个函数的时候,除了返回对应的值。还对调用函数产生了附加的影响,比如修改了全局变量,修改了参数,修改了外部的存储。

判断以下函数是不是纯函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function sum(a, b) {
return a + b;
}

function printInfo(info) {
console.log(info.name, info.age, info.msg);
//下面这句话让函数不再是纯函数
// info.flag = '打印结束 哈哈哈';
}
let obj = {
name: 'alex',
age: 99,
msg: '哈哈哈哈',
};
printInfo(obj);

// if (obj.flag) {
// xxx;
// } else {
// xxx;
// }

例子

  • slice:截取数组,不会对原数组进行任何操作,而是返回一个新数组给你。是纯函数
  • splice:截取数组,返回一个新数组给你,就是已经改变的原数组。不是纯函数

1.2.2 纯函数的优势

  • 安心的编写 安心的使用
1
2
3
4
5
6
7
8
let counter = 0;
function add(num) {
//不能,需要关注外部的counter的变化
// return num+counter

//可以随便用,你外部的counter哪怕变成一个对象/数组 都和我没关
return num;
}

1.3 柯里化的理解和应用

1.3.1 柯里化的理解

概念:在计算机科学中,柯里化(Currying)是把接收多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数且返回结果的新函数的技术。

f(a,b,c) 转成f(a)(b)(c)

注意:柯里化不会调用函数,只是对函数进行转化

1.3.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
function foo1(x, y, z) {
console.log(x + y + z);
}
foo1(10, 20, 30);
// foo1(10)(20)(30)
function foo2(x) {
return function (y) {
return function (z) {
console.log(x + y + z);
};
};
}
foo2(10)(20)(30);
//箭头函数
// function foo3(x) {
// return (y) => {
// return (z) => {
// console.log(x + y + z);
// };
// };
// }
let foo3 = (x) => (y) => (z) => {
console.log(x + y + z);
};
foo3(10)(20)(30);

1.3.3 柯里化函数优势

  1. 函数的职责单一
  2. 函数的参数复用

1.3.4 柯里化函数练习

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
//打印一些日志
//日志的时间 日志的类型:info/debug/feature 具体的信息
// //没有柯里化
// function logInfo(date, type, msg) {
// console.log(`时间${date}-类型${type}-内容${msg}`);
// }
// //打印日志
// logInfo('2023-05-16', 'debug', '修复样式bug');
// //又修复了一个bug
// logInfo('2023-05-16', 'debug', '修复了接口的bug');
//柯里化
function logInfo(date) {
return function (type) {
return function (msg) {
console.log(`时间${date}-类型${type}-内容${msg}`);
};
};
}
// logInfo(时间)(类型)(内容)
var logToday = logInfo('2023-05-16'); //logInfo(时间)()()
//定义debug的日志
var logTodayDebug = logToday('debug'); //logInfo(时间)(类型)()
//定义feature的日志
var logTodayFeature = logToday('feature'); //logInfo(时间)(类型)()
var logTodayInfo = logToday('info'); //logInfo(时间)(类型)()
//今天 修改 bug 唯一变化 内容
logTodayDebug('修复按钮点击');
logTodayDebug('修复接口');
logTodayDebug('修复排序');
logTodayFeature('新增过滤');
logTodayFeature('新增统计功能');

1.3.5 自动柯里化函数

如何定义一个函数可以将多个参数的普通函数转成柯里化的函数

1.4 组合函数的理解和应用

1.4.1 组合函数概念的理解

把依次调用的多个函数组合起来,让他们自动的依次调用,这个过程就是组合函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var num = 100;
//翻倍
function double(num) {
return num * 2;
}
//求平方
function pow(num) {
return num ** 2;
}
console.log(pow(double(123)));
//把上述两个函数结合起来
function composeFn(num) {
return pow(double(num));
}
console.log(composeFn(123));

1.4.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
33
34
35
36
37
38
39
40
41
42
43
44
/* 
考虑更加复杂的情况,比如传入了更多的函数,比如调用composeFn的时候传入了更多的参数
*/

//翻倍
function double(num) {
return num * 2;
}
//求平方
function pow(num) {
return num ** 3;
}
//封装:传入两个函数
function composeFn(...fns) {
//用来做边界判断
let length = fns.length;
if (length <= 0) return;
for (let i = 0; i < length; i++) {
//此刻fn就是每一个函数
let fn = fns[i];
if (typeof fn !== 'function') {
throw new Error(`第${i}个参数必须是一个函数`);
}
}

//返回并调用新函数
return function (...args) {
// let res = fns[0](...args)
let res = fns[0].apply(this, args);
for (let i = 1; i < length; i++) {
let fn = fns[i];
// res = fn.apply(this, [res]);
res = fn(res);
}
return res;
};
// return pow(double(num));
}
let newFn = composeFn(double, pow, console.log);
newFn(100);

let doubleRes = double(100);
let powRes = pow(doubleRes);
console.log(powRes);

1.5 with、eval

with:扩展一个语句的作用域链
eval:执行字符串格式的 js 代码

1.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
25
26
27
28
'use strict';
//1.不会意外的创建全局变量
// function foo() {
// abc = '123';
// }
// foo();
// console.log(abc);
//2. 发现静默的错误
// var obj = {
// name: 'alex',
// };
// Object.defineProperty(obj, 'name', {
// writable: false,
// configurable: false,
// });
// obj.name = 'dog';
// console.log(obj.name);
//3. 参数名称不能相同
// function foo(num, num) {}
//4. 数字不能以0开头
// console.log(0123);
//5.eval
//6. this
function foo() {
console.log(this);
}
foo(); //undefined
foo.apply('abc'); //String {'abc'}

2.对象

2.1 Object.defineProperty

对对象的属性做精准的控制,属性描述符
Object.defineProperty(obj,prop,desciptor)
obj 要定义属性的对象
prop 属性名
desciptor 对该属性的描述

desciptor 有两种类型

  1. 数据属性 Data
  2. 存取属性 Accessor

详细的说明 :https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty

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
var obj = {
name: 'alex',
address: '天河',
};
// Object.defineProperty(obj, 'name', {
// configurable: false, //该属性不可被删除
// enumerable: false, //obj的name属性不可被枚举 for in Object.keys
// writable: false, //该属性不能修改
// value: '123', //返回的值
// });
// delete obj.name;
// for (const key in obj) {
// console.log(key);
// }
// obj.name = 'dog';
// console.log(obj.name);
// //添加新的属性
// Object.defineProperty(obj, 'height', {
// value: 1.8,
// });
let _name;
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: false,
//设置属性值时会自动调用
set: function (value) {
console.log('set被调用', value);
_name = value;
},
//获取 时 自动调用
get: function () {
console.log('get被调用了');
return _name;
},
});

obj.name = '123';
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//  让 a == 1 && a == 2 && a == 3; 为true
let i = 1;
Object.defineProperty(window, 'a', {
get: function () {
return i++;
},
});
console.log(a == 1 && a == 2 && a == 3);

let obj = {};
//定义多个新的属性
Object.defineProperties(obj, {
name: {
configurable: true,
enumerable: true,
writable: true,
value: 'alex',
},
age: {},
height: {},
});

2.2 一些新方法

  • 禁止对象扩展属性
  • 密封对象 不允许你配置和删除属性
  • 冻结对象 不允许修改现有属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 'use strict';
var obj = {
name: 'alex',
age: 18,
};
//不允许扩展
// Object.preventExtensions(obj);
// obj.address = '123';
// console.log(obj);

//密封
// Object.seal(obj);
// delete obj.age;
// console.log(obj);

//冻结对象
Object.freeze(obj);
obj.name = 'bbb';
console.log(obj);

1. 对象方法的补充

1.1 hasOwnProperty

对象是否有某个属于自己的属性(不是原型上的)

1
2
3
4
5
6
7
let obj = {
name: 'alex',
age: 18,
};
obj.__proto__.address = '天河';
console.log(obj.hasOwnProperty('name')); //true
console.log(obj.hasOwnProperty('address')); //false

1.2 in 操作符

判断某个属性是否在某个对象或者对象的原型上

1
2
3
4
5
6
7
8
9
10
11
let obj = {
name: 'alex',
age: 18,
};
obj.__proto__.address = '天河';
console.log('name' in obj); //true
console.log('address' in obj); //true
//for in 遍历的时候不仅仅是对象自身的也包括原型上的
for (const key in object) {
console.log(key);
}

1.3 instanceof

用于检测构造函数的 prototype 是否出现在某个实例对象的原型链上
就是用于判断对象和(类)构造函数之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
function Person() {}
function Student() {
//为了把父构造函数体内的内容拿到
Person.call(this);
}
//为了拿到原型上的内容
Student.prototype = new Person();
var stu = new Student();
console.log(stu instanceof Student); //true
console.log(stu instanceof Person); //true
console.log(stu instanceof Function); //false
console.log(stu instanceof Object); //true
console.log(stu instanceof Array); //false

1.4 isPrototypeOf

用于检测某个对象,是否出现在某个实例对象的原型链上
可以判断对象之间的继承关系

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
function Person() {}
function Student() {
//为了把父构造函数体内的内容拿到
Person.call(this);
}
//为了拿到原型上的内容
Student.prototype = Person();
var stu = new Student();
console.log(Student.prototype.isPrototypeOf(stu)); //true
console.log(Person.prototype.isPrototypeOf(stu)); //true

//判断对象之间的继承关系
var obj = {
name: 'alex',
age: 28,
};
function createObject(o) {
function F() {}
F.prototype = o;
return new F();
}
//info是F的实例对象,obj赋值为F的原型
//info继承obj info子对象 obj父对象
let info = createObject(obj);
console.log(obj.isPrototypeOf(info)); //true

2. class 方式定义类

2.1 认识 class 定义类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//es5定义类
// function Person(){}

//es6
// {key:value} 对象
// {表达式} 代码块
// {}->类的结构
class Person {}
let alex = new Person();
let jack = new Person();
console.log(alex, jack);

//基本不用
const Student = class {};
const foo = function () {};
let aa = new Student();
console.log(aa);

2.2 class 类中的内容

new 的时候发生了什么事:会调用构造函数的 constructor,并执行如下操作,在内存中创建一个新的空对象;这个对象内部的__proto__属性会被赋值该构造函数的 prototype 属性;构造函数内的 this 会指向创建出来的新对象;执行构造函数体内的代码;如果构造函数没有返回非空对象,则返回创建出来的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
//当我们通过new操作符,默认调用class中的constructor
//名称固定的,而且每个class只能有一个constructor
constructor(name, age) {
this.name = name;
this.age = age;
}
//实例方法
//本质上 放在Person.prototype上
running() {
console.log(this.name + '疯狂的跑');
}
eating() {
console.log(this.name + '嘎嘎炫');
}
}

let obj = new Person('alex', 18);
console.log(obj);
console.log(Person.prototype === obj.__proto__); //true
console.log(Person.running);
console.log(Person.prototype.running);

2.3 类和构造函数的区别

class 定义的类不能作为普通函数去调用,必须和 new 连用

2.4 类的访问器方法

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
46
47
48
49
50
51
52
53
//对象的访问器
let obj = {
_name: 'alex',
};
Object.defineProperty(obj, 'name', {
configurable: true,
enumerable: true,
set() {},
get() {},
});
let obj2 = {
_name: 'alex',
set name(value) {
this._name = value;
},
get name() {
return this._name;
},
};
//类的访问器
class Person {
//coder之间的约定 _开头的属性方法,不在外界访问
constructor(name, age) {
this._name = name;
}
set name(value) {
console.log('设置name');
this._name = value;
}
get name() {
console.log('获取name');
return this._name;
}
}
let p1 = new Person('alex', 18);
//访问器的应用场景
class Rectangle {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
get position() {
return { x: this.x, y: this.y };
}
get size() {
return { width: this.width, height: this.height };
}
}
let rect1 = new Rectangle(10, 20, 100, 200);
console.log(rect1.position);
console.log(rect1.size);

3. extends 实现继承

3.1 继承

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
//定义父类
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
running() {
console.log('明天🏃');
}
eating() {
console.log('明天吃KFC');
}
}
class Student extends Person {
constructor(name, age, sno, score) {
//子类的构造函数
//super必须写在所有的this之前
super(name, age);
this.sno = sno;
this.score = score;
}
studying() {
console.log('不开灯的卷');
}
}
let stu = new Student('alex', 18, 111, 59);
stu.eating();
stu.running();
stu.studying();

3.2 super 关键字

super 使用的位置有三个,子类的构造函数,实例方法,静态方法

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
class Animal {
running() {
console.log('🏃');
}
eating() {
console.log('🍽️');
}
//静态方法,给类用的
static sleep() {
console.log('没日没夜的睡');
}
}
class Dog extends Animal {
//子类如果对父类的方法不满意(继承过来的方法)
//重新实现--重写
running() {
console.log('四条腿跑');
//调用父类方法
super.running();
}
static sleep() {
console.log('趴着');
super.sleep();
}
}
let dog = new Dog();
dog.running();
dog.eating();
Dog.sleep();

3.3 继承内置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//队列 第一个元素  最后一个元素
// class myArray extends Array {
// get lastItem() {
// return this[this.length - 1];
// }
// get firstItem() {
// return this[0];
// }
// }
// let arr = new myArray(10, 20, 30);
// // console.log(arr.firstItem, arr.lastItem);

//另一种方式
Array.prototype.lastItem = function () {
return this[this.length - 1];
};
let arr = new Array(10, 20, 30);
console.log(arr.__proto__ === Array.prototype);
console.log(arr.lastItem());

4. 多态概念的理解

面向对象的三大特性:封装、继承、多态
私人:不同数据类型,同一个操作,表现出不同的行为,就是多态的体现

1
2
3
4
5
function sum(a, b) {
console.log(a + b);
}
sum(1, 2); //3
sum('1', 2); //12

5. 补充

5.1 对象字面量的增强写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/* 
1. 属性
2. 方法
3. 计算属性名
*/
let name = 'alex';
let age = 18;
let key = 'address' + 'city';
let obj = {
name,
age,
eating() {},
[key]: '广州',
};
console.log(obj);

5.2 回顾函数的原型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//foo.__proto__ === Function.prototype
function foo(name, age) {
console.log(this, name, age);
}
function test() {}
//函数对象中的某些方法来自于Function.prototype
foo.apply('abc', ['alex', 18]);
console.log(foo.apply === Function.prototype.apply);
//我在 Function.prototype添加的属性和方法 所有函数都能用
Function.prototype.info = 'hello alex';
foo.info;
test.info;
Function.prototype.bar = function () {
console.log('bar~~~~~~');
};
foo.bar();
test.bar();
//Array.prototype.slice.apply(arguments)
// [].slice.apply(arguments)

5.3 手写 apply

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
//apply的实现
function foo(name, age) {
console.log(this, name, age);
}

// foo.apply('aaa', ['alex', 18]);
//1.给所有函数添加myapply
Function.prototype.myapply = function (thisArg, otherArgs) {
//thisArg->this
//otherArgs剩下的参数 -> 那个数组
//获取thisArg,确保他是一个对象类型
thisArg =
thisArg === null || thisArg === undefined ? window : Object(thisArg);
//调用前面的函数
//定义对象上的方法,我之后调用
Object.defineProperty(thisArg, 'fn', {
enumerable: false,
configurable: true,
//具体fn里面放的是什么
value: this,
});
thisArg.fn(...otherArgs);
//调用完以后删除fn 避免副作用
delete thisArg.fn;
};
foo.myapply('aaa', ['alex', 18]);
foo.myapply('aaa', ['alex', 18]);
foo.myapply(null, ['alex', 18]);

5.4 手写 call

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
//call的实现
function foo(name, age) {
console.log(this, name, age);
}

// foo.apply('aaa', ['alex', 18]);
//1.给所有函数添加mycall
Function.prototype.mycall = function (thisArg, ...otherArgs) {
//thisArg->this
//otherArgs剩下的参数 -> 那个数组
//获取thisArg,确保他是一个对象类型
thisArg =
thisArg === null || thisArg === undefined ? window : Object(thisArg);
//调用前面的函数

//定义对象上的方法,我之后调用
Object.defineProperty(thisArg, 'fn', {
enumerable: false,
configurable: true,
//具体fn里面放的是什么
value: this,
});
thisArg.fn(...otherArgs);
//调用完以后删除fn 避免副作用
delete thisArg.fn;
};
foo.mycall('aaa', 'alex', 18);
foo.mycall('aaa', 'alex', 18);
foo.mycall(null, 'alex', 18);

5.5 call-apply 抽取封装

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
//call的实现
function foo(name, age) {
console.log(this, name, age);
}
// 封装到独立的函数中
// function execFn(thisArg, otherArgs, fn) {
// //保证thisArg是一个对象
// thisArg =
// thisArg === null || thisArg === undefined ? window : Object(thisArg);
// // thisArg.fn = this;
// Object.defineProperty(thisArg, 'fn', {
// enumerable: false,
// configurable: true,
// value: fn,
// });
// thisArg.fn(...otherArgs);
// delete fn;
// }
//封装到原型上 我的exec里面已经能call 也能apply了
Function.prototype.myexec = function (thisArg, otherArgs) {
thisArg =
thisArg === null || thisArg === undefined ? window : Object(thisArg);
Object.defineProperty(thisArg, 'fn', {
enumerable: false,
configurable: true,
value: this,
});
thisArg.fn(...otherArgs);
delete thisArg.fn;
};
//1. 给每个函数添加 myapply
Function.prototype.myapply = function (thisArg, otherArgs) {
this.myexec(thisArg, otherArgs);
};
//2. 给每个函数添加 mycall
Function.prototype.mycall = function (thisArg, ...otherArgs) {
this.myexec(thisArg, otherArgs);
};
foo.mycall('aaa', 'alex', 18);
foo.myapply('aaa', ['alex', 18]);
// foo.mycall(null, 'alex', 18);

手写 bind

相比上面,不用调用而是返回这个 fn

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo(name, age, height, address) {
console.log(this, name, age, height, address);
}
Function.prototype.mybind = function (thisArg, ...otherArgs) {
thisArg =
thisArg === null || thisArg === undefined ? window : Object(thisArg);
Object.defineProperty(thisArg, 'fn', {
enumerable: false,
configurable: true,
value: this,
});
// thisArg.fn(...otherArgs);
// delete thisArg.fn;
//我要返回一个fn,而不是调用他
//他的实参个数确定吗?不确定 rest
return (...newArgs) => {
//原来的参数 我也不能丢
let allArgs = [...otherArgs, ...newArgs];
thisArg.fn(...allArgs);
};
};
let newFoo = foo.bind('abc', 'alex', 18);
newFoo(1.88, 18888);

ES6

1. 函数

1.1 默认参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//es6开始有默认值了 默认参数不会对null做处理
function foo(arg1, arg2) {
// //第一种写法
// arg1 = arg1 ? arg1 : '我是默认值';
// //第二种写法
// arg1 = arg1 || '我是默认值';
// //上述两种写法都不够严谨
// arg1 = arg1 === undefined || arg1 === null ? '我是默认值' : arg1;
// console.log(arg1);

//es6+的语法 ??能够处理null
arg1 = arg1 ?? '我是默认值';
console.log(arg1);
}
foo(123);
foo('123');
foo('');
foo(false);
foo(null);
foo(0);
foo(undefined);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//最好把有默认参数的形参放到最后面,但还是会按照顺序来匹配
//默认值会改变函数的length,默认值以及后面的参数都不算在length中
function foo(age, name, height = 1.88, ...args) {
console.log(name, age, height);
}
foo(18, 'alex');

//默认值可以和解构一起使用
// const obj = { name: 'alex' };
// const { name = 'jack' } = obj;
// function bar(obj = { name: 'alex', age: 18 }) {
// console.log(obj.name, obj.age);
// }
// bar();
let obj = {
name: 'alex',
age: 18,
};
function bar({ name, age } = obj) {
console.log(name, age);
}
bar();

1.2 箭头函数补充

1
2
3
4
5
6
7
8
9
10
function foo() {}
let f = new foo();
console.log(foo.__proto__); //Function.prototype
console.log(foo.prototype === f.__proto__); // new Foo()-> f.__proto__ === Foo.prototype

let bar = () => {};
console.log(bar.__proto__ === Function.prototype); //true
//箭头函数没有显示原型
console.log(bar.prototype); //undefined
let b = new bar(); //报错

2. 展开运算符

2.1 展开的基本使用

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 str = 'helloworld';
console.log(...str);
//1.数组构造时使用
const names = ['abc', 'cba', 'nba', 'mba'];
const newNames = [...names, 'aa', 'bb'];
//2. 函数的剩余参数
function foo(name1, name2, ...args) {
console.log(name1, name2, args);
}
//3. 函数调用时使用
foo(...names);
foo(...str);
//4. 对象
const obj = {
name: 'alex',
age: 18,
};
const info = {
...obj,
height: 1.88,
};
//不能这样做, 因为obj无法迭代
foo(...obj);
//可迭代对象:数组/string/arguments

2.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
33
34
const obj = {
name: 'alex',
friend: {
name: 'autoCurry',
},
};
// //引用赋值
// const info = obj;
// // info.name = 123;
// //浅拷贝:只拷贝第一层
// const info1 = {
// ...obj,
// };
// info1.name = '哈哈哈';
// console.log(obj.name); //alex
// console.log(info1.name);
// info1.friend.name = 'mybind';
// console.log(obj.friend.name); //mybind
// console.log(info1.friend.name); //mybind

//深拷贝
//1.库
//2.自己实现
//3.利用现有机制
//把对象序列化
// let a = JSON.stringify(obj);
// console.log(a);
// //反序列化
// let b = JSON.parse(a);
// console.log(b);
let info2 = JSON.parse(JSON.stringify(obj));
// console.log(info2);
info2.friend.name = '我是info2哈哈哈';
console.log(obj, info2);

3. Symbol

生成独一无二的值

3.1 初识 symbol

1
2
3
4
5
6
7
8
9
const s1 = Symbol();
const obj = {
name: 'aaaaaaalex',
[s1]: 'aaa',
};
const s2 = Symbol();
console.log(s1 == s2); //false
obj[s2] = 'bbb';
console.log(obj);

3.2 symbol 注意和 api

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
const s1 = Symbol();
const s2 = Symbol();
const obj = {
name: 'alex',
[s1]: 'aa',
[s2]: 'bb',
};
const obj1 = {};
obj1[s1] = 'aa';
obj1[s2] = 'bb';

const obj2 = {};
Object.defineProperty(obj2, s1, {
value: 'aaa',
});
//有方法获取symbol的key
console.log(Object.keys(obj)); //拿不到symbol的key
console.log(Object.getOwnPropertySymbols(obj)); //只能拿symbol的key
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const key of symbolKeys) {
console.log(obj[key]);
}

const s3 = Symbol('ccc');
console.log(s3);
//相当于是Symbol的key
console.log(s3.description);
const s4 = Symbol(s3.description);
//哪怕是同一个key 还是false
console.log(s3 === s4); //false

//我非要相同的Symbol
//如果你有相同的key,可以通过Symbol.for生成相同的Symbol值
const s5 = Symbol.for('ddd');
const s6 = Symbol.for('ddd');
console.log(s5 === s6); //true

//获取传入的key,后面可能用key生成相同的symbol
console.log(Symbol.keyFor(s5));

4. Set 集合

类似数组,但是最大区别是天生不允许重复。只能通过构造函数来创建

4.1 set

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
46
47
48
49
50
const set = new Set();
// console.log(set);
//添加元素
set.add(10);
set.add(20);
set.add(22);
//做了添加重复的元素,但是没用
set.add(22);
console.log(set);
// const info = {};
const obj = {};
// set.add(info);
// set.add(obj);
//可以添加,obj和info是两个不同的对象
// console.log(set);
// set.add(obj); //没用,因为重复
// console.log(set);
//set的应用场景 去重
const names = ['abc', 'abc', 'cba', 'cba', 'nba', 'mba'];
// const newNames = [];
// for (const item of names) {
// if (!newNames.includes(item)) {
// newNames.push(item);
// }
// }
const newNamesSet = new Set(names);
const arr = Array.from(newNamesSet);
//更简单
// console.log([...new Set(names)]);
//size 里面元素的个数

//add添加
set.add(100);
console.log(set);

//delete 删除
set.delete(100);
console.log(set);

//清空 clear
// set.clear();

//has 有某个元素
console.log(set.has(obj)); //false

set.forEach((item) => console.log(item));

for (const item of set) {
console.log(item);
}

4.2 WeakSet

weak Reference 弱引用:可以通过引用获取到对应的数据,但是他引用的那个对象不保证一定不被销毁.
strong Reference 强引用:我保证你引用的对象永远存在

1
2
3
4
5
6
7
8
9
10
11
let obj1 = { name: 'alex' };
let obj2 = { name: 'jack' };
let obj3 = { name: 'kern' };
let arr = [obj1, obj2, obj3];
obj1 = null;
obj2 = null;
obj3 = null;
//set对象对obj1 2 3 有引用,所以这三个对象不会被销毁
const set = new Set(arr);
arr = null;
console.log(set);
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
46
47
let obj1 = { name: 'alex' };
let obj2 = { name: 'jack' };
let obj3 = { name: 'kern' };
let arr = [obj1, obj2, obj3];
// obj1 = null;
// obj2 = null;
// obj3 = null;
const weakset = new WeakSet();
//WeakSet只能添加对象不能存放基本数据类型
// weakset.add(123);
weakset.add(obj1);
weakset.add(obj2);
weakset.add(obj3);
//不一定能访问到,因为weakset是弱引用,里面的东西可能会被销毁掉
console.log(obj1);
//add delete has

//WeakSet不能遍历

//应用 stackoverflow 集合
const pWeakSet = new WeakSet();
class Person {
constructor() {
pWeakSet.add(this);
}
//问题很大,放在类里面的方法,我只希望你的实例调用的
//引用给谁 谁就能调用
//限制一下 只让他的实例调用
running() {
if (!pWeakSet.has(this)) {
console.log('type error 调用方式不对');
} else {
console.log('跑~');
}
}
}
//都能正常调用
let p = new Person();
p.running();
const fn = p.running;
fn();
const obj = {
run: p.running,
};
obj.run();
//为什么不用set,set强引用,p = null set里面的引用 一直在 对象一直在
//weakset 我想销毁 就能正常销毁

WeakSet 只能存放对象类型,不能 add 基本类型
WeakSet 是弱引用。
WeakSet 不能遍历,原因是:WeakSet 是弱引用,如果遍历获取其中的元素,有可能导致对象不能正常的销毁。

5. Map 映射

5.1 Map

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
//没有map 对象不能当作键名
const info = { name: 'alex' };
const info2 = { name: 'jack' };
// const obj = {
// address: '火星',
// [info]: 'hhahah',
// [info2]: '嘻嘻嘻嘻',
// };
// console.log(obj);
// //[object Object]是字符串,100%会遇到这个
// //我只能接受字符串,你却给我一个对象,我没办法了就展示成这样

const map = new Map();
//设置 set
map.set(info, 'aaaa');
map.set(info2, 'bbb');
console.log(map);

//获取 get
console.log(map.get(info));

//删除 delete
// map.delete(info);

// 清空 clear()

//has
console.log(map.has(info));

//forEach 值
map.forEach((item) => console.log(item));

for (const item of map) {
console.log(item); //是数组
const [key, value] = item;
console.log('解构后', key, value);
}
//map让对象可以作为key

5.2 WeakMap

区别一:WeakMap 只能用对象做 key,不接受其他类型
区别二:WeakMap 弱引用。

Proxy-Reflect

监听对象的操作(ES5)

  • 首先Object.defineProperty设计的初衷,不是为了数据劫持和数据代理,也就是不是为了监听对象中所有的属性,详细地定义对象的属性,只不过我们的操作强行让它能够监听而已。
  • 其次我们想监听一些更加丰富的操作的时候,没办法。所以 ES6 给我们提供了 proxy

监听对象的操作(ES6)

Proxy 类,用于帮助我们创建一个代理,监听对象的时候,我们可以先创建一个代理对象(proxy),之后所有对该对象的操作都通过代理对象来完成。
const p = new Proxy(target,handler)
target:你要代理的目标对象
hanlder:你需要做那些处理

set: 四个参数
target 目标对象
property 属性
value 值
receiver 调用的代理对象

get: 三个参数
target 目标对象
property 属性
receiver 调用的代理对象

Reflect 和 Object

Reflect(反射)也是 ES6 新增的 API,它是一个对象。提供了操作对象的方法

1
2
3
4
5
6
7
8
const obj = {};
//确实可以这样做,但是不符合ecma标准
console.log(obj.__proto__);
//按照ecma的标准
console.log(Object.getPrototypeOf(obj));

//还可以用reflect
console.log(Reflect.getPrototypeOf(obj));
  • 明明已经有 Object 可以做这些操作,为什么还要有 Reflect?这是因为早期 ECMA 规范没有考虑到对对象本身的操作如何设计 更加好。Object 在 js 内属于顶级的类,Array,Function,Number,String…都继承于他,从语言设计层面来说,作为所有类的父类,你本身不应该包含太多东西。
  • ES6 新增的 Reflect 就把一些操作集中到了 Reflect 对象上。Reflect 就是专门来做对象的操作用的,Proxy 也可以不操作原对象

Promise

Promise 的概念

Promise 是一个类,承诺,许诺
当我们需要的时候,给予调用者一个承诺,待会给你回调的数据,就可以来创建一个 promise 的实例
通过 new 创建 Promise 对象的时候,传入了一个回调函数,称为

Promise是一个用来处理不会立即得到数据或结果(异步操作)的一个构造函数

executor(执行函数)

  • 这个回调函数会被立即执行,并且接受另外的两个回调函数 resolve,reject
  • 当我们调用 resolve 时,会执行 promise 对象的 then 方法传入的回调函数
  • 当我们调用 reject 时,会执行 promise 对象的 catch 方法传入的回调函数

promise 的三个状态

  • 待定(pending):初始状态,即没有被兑现,也没有被拒绝;当执行 executor 中的代码时,处于该状态。
  • 已兑现(fulfilled):意味着操作完成,执行了 resolve,处于该状态的话 promise 已经被兑现了
  • 已拒绝(rejected):意味着操作失败,执行了 reject,处于该状态的话 promise 已经被拒绝了

executor(执行函数)

我们往往会在 executor 中确定 promise 的状态-叫已决议
注意:一旦状态被确定下来,Promise 状态就会被锁死 🔒,该 Promise 的状态就不可变,在我们调用 resolve 的时候,如果 resolve 的值不是一个 Promise,那么会将该 Promise 的状态变成成功。resolve 调用之后再 reject 调用,reject 的代码会执行,但是不会改变 Promise 的状态,并不是不执行代码.

resolve 参数的三种情况

then

then 方法是返回一个全新的 promise,这个 promise 他的状态是等到 then 传入的回调函数有返回值时,再进行决议。

finally

ES9(ES2018)中新增的一个特性,无论你的 promise 对象是 fulfilled 还是 rejected 状态,最终都要执行的代码。finally 不接收参数。

Promise的类方法

Promise.resolve

最大的作用,就是把一个现成的内容转成 Promise来使用,就可以使用 Promise.resolve。
作为 resolve 来说,他的参数是有三种情况 04.html 讲过

Promise.reject

reject 方法类似于 resolve
注意:不管传入 Promise.reject 的参数是何种形式,都会直接作为 rejected 状态给到 catch 的回调

其他类方法…

都是处理多个 promise 的类方法
all allSettled any race

async

async关键字定义的函数叫做异步函数,返回一个Promise对象

语法:

1
2
3
4
5
6
7
8
9
10
//普通写法
async function foo() { }

//箭头函数写法
let bar=async()=>{}

//在claas中的写法
class foo {
async bar() { }
}

await

await关键字必须和async一起使用才有效果。如果有多个await,则会等上一个await的promise对象的状态为兑现状态时,才会执行下一个await后面的表达式

特点:await会阻塞async函数后面代码的执行,直到await后面的表达式结果出来后才会执行后面的代码

await返回的值就是await后面表达式的值

  • 如果await后面表达式是一个普通值,则返回值就是这个普通值
  • 如果await后面表达式是一个兑现状态(fulfilled)的promise对象,则返回值就是兑现状态的值
1
2
3
4
5
6
7
8
9
10
async function foo() {
let res1 = await 123
console.log(res1);
let res2 = await new Promise((reslove, reject) => {
reslove("success")
})
console.log(res2);
}

foo()