前高频端面试题合集3-数据存储

1. javaScript数据类型

1.1 共8种

前面的 7 种数据类型称为原始类型,把最后一个对象类型称为引用类型

2. 判断数据类型的几种方法

2.1 typeof(常用于判断基本数据类型,对于引用数据类型全部返回Object)

  • 语法: typeof [检测数据] 返回数据的类型名称

  • 特点

    • 对于基本类型,除 null 以外,均可以返回正确的结果。
    • 对于引用类型,除 function 以外,一律返回 object 类型。
    • 对于 null ,返回 object 类型。
    • 对于 function 返回 function 类型。

2.2 instanceof(检测某一个实例的原型链上是否有这个类的原型属性)

  • 语法:[监测数据] instanceof [class] :返回true或false

  • 特点:

    • 可以区分复杂数据类型
    • 只要在当前实例的原型链上,我们用其检测出来的结果都是true
    • 基本类型值检测繁琐,且检测不全(undefined, null, symbol)
  • 原理:验证当前类的原型prototype是否会出现在实例的原型链__proto__上,只要在它的原型链上,则结果都为true。因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,找到返回true,未找到返回false

  • 手写instanceof

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function myinstanceOf_(left, right) {
    let proto = left.__proto__;
    let prototype = right.prototype
    while (true) {
    if (proto == null) return false
    if (proto == prototype) return true
    proto = proto.__proto__;
    }
    }

2.3 constructor (用于引用数据类型)

  • 语法: 检测数据.constructor === class
  • 特点:
    • 适合使用在引用数据类型上
    • 原型链不会干扰
  • 原理:构造函数原型上有一个 constructor 属性指向构造函数自身的,如果在实例上使用 construtor 时,就会直接使用其构造函数原型的上的该属性,并指向其构造函数。

2.4 Object.prototype.toString.call()(对象原型链判断方法)

  • 语法:Object.prototype.toString.call(检测数据)
  • 特点:适用于所有类型的判断检测
  • 原理:Object.prototype.toString 表示一个返回对象类型的字符串,call()方法可以改变this的指向,那么把Object.prototype.toString()方法指向不同的数据类型上面,返回不同的结果

3. 内存空间

3.1 内存空间分配

在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间和堆空间

代码空间主要是存储可执行代码

3.2 栈和堆空间

1
2
3
4
5
6
7
function foo(){ 
var a = "闷倒驴";
var b = a;
var c = {name:"王美丽"};
var d = c;
}
foo()

为什么不能把存储在堆中的数据存储在栈中?

因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

堆栈特点

  • 栈空间都不会设置太大,主要用来存放一些原始类型的小数据。而引用类型的数据占用的空间都比较大,所以这一类数据会被存放到堆中,堆空间很大,能存放很多大的数据,不过缺点是分配内存和回收内存都会占用一定的时间

  • 原始类型的赋值会完整复制变量值,而引用类型的赋值是复制引用地址

3.3 数据的赋值与引用数据类型的深浅拷贝

赋值:简单数据类型直接在栈中开辟一块新的内存,存储赋值的数据;引用数据类型,在栈中开辟一块空间,存储赋值的数据对应的堆中的存储地址,源数据和拷贝的新数据对应的是同一块堆空间中的数据

浅拷贝:堆栈各开辟一块新空间,栈中存储堆中新开辟的空间的地址,堆中赋值了源数据的数据,如果值是基本数据类型那么新数据和源数据没有任何关系,如果值是引用数据类型那么新数据的值指向的源数据的数据存储地址

深拷贝:堆栈各开辟一块新空间,栈中存储堆中新开辟的空间的地址,堆中存储的数据和源数据一样,但是二者没有任何联系

赋值例子:

1
2
3
4
5
var user = {userName:'闷倒驴',sex:'女',body:{weight:'50kg',height:'160'}}
// 赋值
var userCopy = user;
user.sex = '男';
user.body.weight = '100斤';
image-20210607171856700

浅拷贝例子:

1
2
3
4
5
var user = {userName:'闷倒驴',sex:'女',body:{weight:'50kg',height:'160'}}
// 浅拷贝
var userCopy = Object.assign({}, user);
user.sex = '男';
user.body.weight = '100斤';

当执行完userCopy浅拷贝之后,内存空间如图所示:

执行完 user.sex = ‘男’; user.body.weight = ‘100斤’; 之后,如下图所示

深拷贝例子:

1
2
3
4
5
var user = { userName: '闷倒驴', sex: '女', body: { weight: '50kg', height: '160' } }
// 深拷贝
var userCopy = JSON.parse(JSON.stringify(user));
user.sex = '男';
user.body.weight = '100斤';

当执行完userCopy深拷贝之后,内存空间如图所示:

当执行完userCopy深拷贝之后,内存空间如图所示:

浅拷贝方法

1.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

1
2
3
4
5
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2.函数库lodash的_.clone方法

该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。

1
2
3
4
5
6
7
8
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
3.展开运算符…

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

1
2
3
4
5
let obj1 = { name: '王美丽', address: { x: 100, y: 100 } }
let obj2 = { ...obj1 }
obj1.address.x = 200;
obj1.name = '闷倒驴'
console.log('obj2', obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
4.Array.prototype.concat()
1
2
3
4
5
6
let arr = [1, 3, {
username: '王美丽'
}];
let arr2 = arr.concat();
arr2[2].username = '闷倒驴';
console.log(arr); //[ 1, 3, { username: '闷倒驴' } ]
5.Array.prototype.slice()
1
2
3
4
5
6
let arr = [1, 3, {
username: ' 王美丽'
}];
let arr3 = arr.slice();
arr3[2].username = '闷倒驴'
console.log(arr); // [ 1, 3, { username: '闷倒驴' } ]

深拷贝方法:

1.JSON.parse(JSON.stringify())
1
2
3
4
5
6
let arr = [1, 3, {
username: '闷倒驴'
}];
let arr1 = JSON.parse(JSON.stringify(arr));
arr1[2].username = '王美丽';
console.log(arr, arr1)

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

比如下面的例子:

1
2
3
4
5
6
let arr = [1, 3, {
username: '闷倒驴'
},function(){}];
let arr1 = JSON.parse(JSON.stringify(arr));
arr1[2].username = '王美丽';
console.log(arr, arr1)
2.函数库lodash的_.cloneDeep方法

该函数库也有提供_.cloneDeep用来做 Deep Copy

1
2
3
4
5
6
7
8
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
3.jQuery.extend()方法

jquery 有提供一個$.extend可以用来做 Deep Copy

1
2
3
4
5
6
7
8
9
// $.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
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
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝,遇到循环引用,将引用存储起来,如果存在就不再拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

循环引用

当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用,(当然不止这一种情况,不过原理是一样的)