JS 基础

JS 数组去重

利用集合
1
2
3
4
5
const arr = [1, 1, 2, 2, 3, 3]

const new_array = Array.from(new Set(arr))

console.log(new_array) // [1, 2, 3]
filter + indexOf

indexOf 返回的始终是元素第一次出现的位置。

1
2
3
4
5
6
7
const arr = [1, 1, 2, 2, 3, 3]

const new_array = arr.filter((val, idx) => {
return arr.indexOf(val) === idx
})

console.log(new_array) // [1, 2, 3]

JS 垃圾回收机制(GC)

概述

垃圾回收机制(Garbage Collection))简称 GC,所谓垃圾回收机制就是清理内存的方式

垃圾回收机制会定期(周期性)找出那些不再用到的内存(变量),然后释放其内存。不再用到的内存,没有及时释放,就叫做内存泄漏(memory leak)。

在 JS 中,我们创建变量的时候,JS 引擎会自动给对象分配对应的内存空间,不需要我们手动分配。当代码执行完毕的时候,JS 引擎也会自动地将你的程序,所占用的内存清理掉。正是因为有垃圾回收机制,才导致了开发者有着不用关心内存管理的错误感觉。

内存的分配机制

JS 数据类型分为两种:

  • 基本数据类型
  • 引用数据类型

基本数据类型保存在固定的栈内存中,可以直接访问它的值。

引用数据类型,其引用地址保存在栈内存中,引用所指向的值保存在堆内存中,需要通过引用地址去访问它的值。

存储在栈内存中的基本数据类型的值,可以直接通过操作系统进行处理。

而堆内存中的引用数据类型的值,大小并不确定,因此需要 JS 引擎的垃圾回收机制进行处理。

内存的回收机制

在浏览器的发展历史上对于垃圾回收有两种解决策略:

  • 标记清除法
    • 从2012年起,所有浏览器都使用了标记清除法。
    • 目前主流浏览器都是使用标记清除式的垃圾回收策略,只不过收集的间隔有所不同。
  • 引用计数法
    • JS引擎很早之前使用过这种策略回收内存。
    • 其核心思想为:将不再被引用的对象(零引用)作为垃圾回收,需要提醒的是,这种策略由于存在很多问题,目前逐渐被弃用了。
标记清除算法

当变量进入执行环境时,就将这个变量标记为”进入环境”,从逻辑上讲,永远不能释放进入环境变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为”离开环境”。

1
2
3
4
5
6
function f1(){
//被标记已进入执行环境
const a = 1
const b = 2
}
f1() //执行完毕,a,b被标记离开执行环境,内存释放

垃圾回收机制在运行的时候会给存储在内存中的所有变量都加上标记(可以是任何标记方式),然后,它会去除掉处在环境中的变量及被环境中的变量引用的变量(闭包)的标记。而在此之后剩下的带有标记的变量被视为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后垃圾回收机制到下一个周期运行时,将释放这些变量的内存,回收它们所占用的空间。

通过标记清除之后,剩下没有被释放的对象在内存中的位置是不变的,这就会导致空闲内存是不连续的,这就造成了内存碎片问题

如果之后需要存储一个新的,需要占据较大连续内存空间的对象的时候,就会造成影响。

标记整理算法

它的标记阶段和标记清除算法没有什么不同,只是标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存。

引用计数算法

该策略的处理过程如下:

  • 当声明一个引用类型并赋值给变量时,这个值的引用次数初始为1
  • 如果该值又被赋值给另一个变量,引用次数+1
  • 如果该变量被其他值覆盖了,引用次数-1
  • 当这个值引用次数变为0时,说明该值不再被引用,垃圾回收器会在运行时清理释放其内存

代码如下:

1
2
3
4
5
let a = new Object() // 引用次数初始化为1
let b = a // 引用次数2,即 obj 被 a 和 b 引用
a = null // 引用次数1
b = null // 引用次数0,
... // GC回收此引用类型在堆空间中所占的内存

但是存在一些问题,例如最常见的是循环引用现象:

1
2
3
4
5
6
7
8
9
10
function fn(){ // fn引用次数为1,因为window.fn = fn,会在window=null即浏览器关闭时回收
let A = new Object() // A: 1
let B = new Object() // B: 1
A.b = B // B: 2
B.a = A // A: 2
}
// A对象中引用了B,B对象中引用了A,两者引用计数都不为0,永远不会被回收。
// 若执行无限多次fn,那么内存将会被占满,程序宕机
fn();
// 还有就是这种方法需要一个计数器,这个计数器可能要占据很大的位置,因为我们无法知道被引用数量的多少。

若是采用标记清除策略则会在 fn 执行完毕后,作用域销毁,将域中的 A 和 B 变量标记为 0以便 GC 回收内存,不会存在这种问题。


JS 原型链

每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,结果会怎样?显然,此时的原型对象将包含一个指向另一个原型的指针,相应地,另一个原型中也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立。如此层层递进,就构成了实例与原型的链条。

prototype 与 proto

prototype (原型)是构造函数才有的,因为 js 的构造函数和一般 function 没有本质区别,所以只有 function 有 prototype 这一属性。

__proto__ 属性任何对象都有,其指向对象的原型(prototype)。

1
2
3
function F() {}
const f = new F();
f.__proto__ === F.prototype // true
函数对象

凡是通过 new Function() 创建的都是函数对象。

一般来说,对象只有 __proto__ 属性,但是函数对象既有 __proto__ 属性,也有 prototype 属性。

函数对象有 Function、Object、Array、Date、String、自定义函数。

1
2
3
4
5
console.log(typeof Object);   //function  
console.log(typeof Array); //function
console.log(typeof String); //function
console.log(typeof Date); //function
console.log(typeof Function); //function
原型对象

原型对象即 XXX.prototype

原型对象是包含特定类型的所有实例共享的属性和方法。原型对象的好处是,可以让所有实例对象共享它所包含的属性和方法。

当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。

1
2
3
4
5
6
7
8
function F() {}
F.prototype.name = "silence";

const f1 = new F();
const f2 = new F();
f2.name = "qzmvc";

console.log(f1.name, f2.name) // "silence" "qzmvc"

一般来说,原型对象的类型都是 object,但是 Function.prototype 是个例外,它是原型对象,却又是函数对象,作为一个函数对象,它又没有 prototype 属性。

1
2
3
console.log(typeof Function.prototype) // 特殊 function  
console.log(typeof Function.prototype.prototype) // undefined
console.log(typeof Function.prototype.__proto__) // object
图解


箭头函数和普通函数区别

  1. 箭头函数全都是匿名函数,普通函数可以有匿名函数,也可以有具名函数;

  2. 箭头函数不能用于构造函数;

  3. 箭头函数没有 arguments 对象;

  4. 箭头函数没有原型对象;

  5. this 指向不同。


JS 继承方式

原型链继承

让一个构造函数的原型是另一个类型的实例,那么这个构造函数 new 出来的实例就具有该实例的属性。

1
2
3
4
5
6
7
function Parent() {
this.name = "xxx";
this.age = 20;
}

function Son() {}
Son.prototype = new Parent();

缺点:对象实例共享所有继承的属性和方法。不能传递参数。

构造函数继承
1
2
3
4
5
6
7
8
function Parent(name, age) {
this.name = name;
this.age = age;
}

function Son() {
Parent.call(this, "silence", 20);
}

使用 apply() 或 call() 方法在子类构造函数中调用父类构造函数,同时将 this 指向 Son。

优点:解决了原型链继承对象实例共享所有继承的属性和方法以及不能传递参数的问题。

缺点:通过原型添加的属性和方法无法继承。

组合继承

所谓组合继承即将原型链继承和构造函数继承组合到一起。

使用原型链实现对原型属性和方法的继承,通过借用构造函数来实现对实例属性的继承。

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

function Son() {
Parent.call(this, "silence", 20);
}
Son.prototype = new Parent();

优点:解决了原型链继承和构造函数继承造成的影响。

缺点:调用两次父类构造函数。

原型继承

Object.create() 方法用于创建一个新对象,使用现有的对象来作为新创建对象的原型(prototype)。

1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: "xxx",
arr: [1, 2, 3]
}

const new_obj = Object.create(obj);
console.log(new_obj) // {} | 由于 obj 是 new_obj 的原型,而不是直接赋值,因此这里为空
console.log(new_obj.name) // "xxx" | 当在 new_obj 中找不到 name 属性时,就去它的原型中找
console.log(new_obj.__proto__) // {name: "xxx", arr: [1, 2, 3]}

new_obj.arr.push(4);
console.log(obj.arr) // [1, 2, 3, 4]
1
2
3
4
5
6
7
8
9
10
11
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sex = "male";

function Son() {}
Son.prototype = Object.create(Parent.prototype)
// 如果未将 Son.prototype.constructor 设置为 Son,
// 它将采用 Parent(父级)的 prototype.constructor。
Son.prototype.constructor = Son

缺点:属性中引用类型的值会在对象间共享;子类实例不能向父类传参;只能继承原型上的的属性和方法。

寄生组合式继承
1
2
3
4
5
6
7
8
9
10
11
function Parent(name, age) {
this.name = name;
this.age = age;
}
Parent.prototype.sex = "male";

function Son() {
Parent.call(this. "xxx", 10)
}
Son.prototype = Object.create(Parent.prototype)
Son.prototype.constructor = Son

JS 判断数据类型方法

使用 typeof

使用 typeof 能够判断出的数据类型有:

  • string

  • number

  • boolean

  • sysmbol

  • undefined

  • function

  • object

基本数据类型中除了 null 都能直接判断出来。

1
typeof null // object

引用数据类型只能判断出 function 和 object。

1
2
typeof [1, 2, 3] // object
typeof new Date() // object
object.prototype.toString.call()

Object.prototype.toString.call() 将要检查的对象作为第一个参数传递,返回 "[object Type]",这里的 Type 就是参数的类型。

1
2
3
4
5
6
7
8
9
10
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(false); // "[object Boolean]"
Object.prototype.toString.call(12); // "[object Number]"
Object.prototype.toString.call(new Date()); // "[object Date]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"

function f() {}
Object.prototype.toString.call(f); // "[object Function]"

手写 bind

1
2
3
4
5
6
7
8
9
Function.prototype.bind = function() {
const _this = this;
const context = arguments[0];
let args = [...arguments].slice(1);
return function() {
args = args.concat([...arguments]);
_this.apply(context, args);
}
}

JS new 一个对象的过程

  1. 创建一个空对象;

  2. 将该对象连接到对应类的原型;

  3. 执行类的构造函数,并将 this 指向该对象,使其拥有类的属性和方法;

  4. 返回该对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function myNew() {
// 创建一个空对象
const obj = {};
// 连接原型
const constructor = arguments[0];
obj.__proto__ = constructor.prototype;
// 执行构造函数
constructor.applay(obj, [...arguments].slice(1));
// 返回对象
return obj;
}

function F(name, age) {
this.name = name;
this.age = age;
}

const f = myNew(F, "silence", 20); // 等价于 new F("silence", 20)

柯里化

概念

柯里化,用一句话解释就是,把一个多参数的函数转化为单参数函数的方法。

当一个函数有多个参数的时候先传递一部分参数调用它(这部分参数以后永远不变),然后返回一个新的函数接收剩余的参数,最后返回结果。

1
2
3
4
5
function plus(x, y){
return x + y
}

plus(1, 2) // 输出 3

经过柯里化后这个函数变成这样:

1
2
3
4
5
6
7
function plus(y){
return function (x){
return x + y
}
}

plus(1)(2) // 输出 3
作用
惰性求值

柯里化的函数是分步执行的,第一次调用返回的是一个函数,第二次调用的时候才会进行计算。起到延时计算的作用,通过延时计算求值,称之为惰性求值。

动态生成函数

看如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
function log(level) {
return function(message) {
console(level + ":" + message)
}
}

const info = log("INFO");
info("xxx") // INFO:xxx
info("yyy") // INFO:yyy

const debug = log("DEGUB");
debug("xxx") // DEBUG:xxx
debug("yyy") // DEBUG:yyy

可以看到,如果我们想打印不同级别的日志,且为每条日志固定日志的级别,通过柯里化可以轻松地为当前日志创建便捷函数。

高级柯里化实现
1
2
3
4
5
6
7
8
9
10
11
12
function curry(func) {

return function curried(...args) {
if (args.length >= func.length) {
return func.apply(this, args);
} else {
return function(...args2) {
return curried.apply(this, args.concat(args2));
}
}
}
}

curry 函数创建一个函数,该函数接收一个或多个 func 函数的参数,如果 func 所需要的参数都被提供则执行 func 并返回执行的结果,否则继续返回该函数并等待接收剩余的参数。

1
2
3
4
5
6
7
8
9
function sum(a, b, c) {
return a + b + c;
}

let curriedSum = curry(sum);

alert( curriedSum(1, 2, 3) ); // 6,仍然可以被正常调用
alert( curriedSum(1)(2,3) ); // 6,对第一个参数的柯里化
alert( curriedSum(1)(2)(3) ); // 6,全柯里化