1. JavaScript数据类型
现在的ECMAScript有7种基本数据类型和引用数据类型。参考:JavaScript 数据类型和数据结构
7 种原始类型:
- Boolean
- Null
- Undefined
- Number
- BigInt
- String
- Symbol
和引用数据类型
- Object
这里只写了一种,但是还有很多其他也是引用数据类型,比如Array、Function、Date、RegExp、Error。
ES6增加了一种基本数据类型
Symbol,数据类型 “symbol” 是一种原始数据类型,该类型的性质在于这个类型的值可以用来创建匿名的对象属性。该数据类型通常被用作一个对象属性的键值——当你想让它是私有的时候,详见 MDN——symbol
BigInt数据类型,参考 MDN——BigInt,可对原本超出数值范围的值进行运算。
1.1. typeof返回值
返回八种数据类型,参考:MDN——typeof
null会返回object,function会返回function
| 返回值(字符串) |
|---|
| "undefined" |
| "object" |
| "boolean" |
| "number" |
| "bigint" |
| "string" |
| "symbol" |
| "function" |
注:强调typeof是一个操作符而非函数,括号可略,null之所以会返回
object是因为null最初是作为空对象的占位符的使用的,被认为是空对象的引用。
实际上undefined值派生自null值,所以undefined == null //true
构造函数的括号也可省略:
console.log(new Number);// [Number: 0]
如果定义的变量将来用于保存对象,那么最好将该变量初始化为null,这样只要检查null值就可以知道相应的变量是否已经保存了一个对象的引用。
注:尽管null和undefined有特殊关系,但他们完全不同,任何情况都没有必要把一个变量值显式地设置为undefined,但对null并不适用,只要意在保存对象的变量还没有真正保存对象,就应该明确保存变量为null值。这样不仅体现null作为空对象指针的惯例,也有助于进一步区分null和undefined。
1.2. 堆内存、栈内存
当变量复制引用类型值的时候,它是一个指针,指向存储在堆内存中的对象(堆内存中的对象无法直接访问,要通过这个对象在堆内存中的地址访问,再通过地址去查值(RHS查询,试图获取变量的源值),所以引用类型的值是按引用访问)
变量的值也就是这个指针(我的意思是这个指针是原始值)是存储在栈上的,当变量obj1复制变量的值给变量obj2时,obj1、obj2只是一个保存在栈中的指针,指向同一个存储在堆内存中的对象,所以当通过变量obj1操作堆内存的对象时,obj2也会一起改变
2. shallowCopy
曾经在面试中遇到过这个问题,面试官问我深拷 贝与浅拷贝的区别。
基本数据类型并没有深浅拷贝之分。但引用类型数据的浅拷贝会创建一个新对象,它有着被拷贝对象属性值的一份精确拷贝。拷贝的是内存地址,所以其中一个值的变化会在另一个上面反映出来。
下面实际中浅拷贝的方法。
2.1. Object.assign()
- 具有相同键时后面的会覆盖前面的;
- 只会拷贝 源对象自身的 且 可枚举的 属性到目标对象;
- (接上一句)所以继承属性、不可枚举属性不能拷贝;
- String、Symbol 类型的属性都会被拷贝;
- 原始类型会被包装为对象;
- 异常会打断后续拷贝任务(例如属性不可写,会引发 TypeError)。
该方法使用源对象的
[[Get]](获得值)和目标对象的[[Set]](设置值),所以它会调用相关getter和setter。因此,它分配属性,而不仅仅是复制或定义新的属性。如果合并源包含getter,这可能使其不适合将新属性合并到原型中。为了将属性定义(包括其可枚举性)复制到原型,应使用Object.getOwnPropertyDescriptor(obj, prop)和Object.defineProperty()。
- Object.getOwnPropertyDescriptor(obj, prop):返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)其中obj——需要查找的目标对象;prop——目标对象内属性名称(字符串)
- Object.defineProperty(obj, prop, descriptor):方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象;其中obj——要在其上定义属性的对象。prop——要定义或修改的属性的名称。descriptor——将被定义或修改的属性描述符。返回值是被传递给函数的对象。
let target = {
a: 1,
b: 2
};
let source = {
b: 4,
c: 5
};
let returnedTarget = Object.assign(target, source);
target.a = 111;
console.log(target);// { a: 111, b: 4, c: 5 }
console.log(returnedTarget);// { a: 111, b: 4, c: 5 }
let obj1 = {
a: {
b: 1
},
sym: Symbol(1)
}
Object.defineProperty(obj1, 'innumerable', {
value: '不可枚举属性',
enumerable: false
})
let obj2 = {};
Object.assign(obj2, obj1);
obj1.a.b = 2;
console.log(obj1); // {a: {…}, sym: Symbol(1), innumerable: "不可枚举属性"}
console.log(obj2); // {a: {…}, sym: Symbol(1)}
const o1 = {
a: 1
}
const o2 = {
a: 2
}
const o3 = {
a: 3
}
const obj = Object.assign(o1, o2, o3);
console.log(obj); //{a: 3}
console.log(o1); //{a: 3}
const o4 = {
a: 32
};
const obj2 = Object.assign(o1, o2, o3, o4);
console.log(obj2); //{a: 32}
2.2. 拓展运算符
拓展运算符对基本数据类型直接创建新值,对引用数据类型shallowcopy。
let obj = { a: 1, b: { c: 1 } }
let obj2 = { ...obj }
obj.a = 2
obj.b.c = 2
console.log(obj) // { a: 2, b: { c: 2 } }
console.log(obj2);// { a: 1, b: { c: 2 } }
扩展运算符Object.assign()有同样的缺陷,对于引用数据类型只是shallowCopy。
但是处理的数据都是基本类型的值的话挺方便。
2.3. Array的slice()
slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(不包括end)。原始数组不会被改变。
语法:arr.slice([begin[, end]])。若省略两个参数将会从头到尾索引
slice 不会修改原数组,只会返回一个shallowCopy的新数组。对于字符串、数字及布尔值来说(区别于 String、Number 或者 Boolean 对象),slice 会拷贝这些值到新的数组里。对于数组元素是引用数据类型,仍是浅拷贝。
let arr1 = [1, "hui", [2], { any: "any" }];
let arr2 = arr1.slice();
arr1[0] = 2;
arr1[1] = "HUI";
arr1[2][0] = 3;
arr1[3].any = "ANY";
console.log(arr1);
console.log(arr2);
2.4. Array的concat()
old_array.concat(value1[, value2[, ...[, valueN]]])。数组和或值将被连接成新数组。如果省略参数,则concat会返回一个它所调用的已存在的数组的浅拷贝。
concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。
let old_array = ["Hui", {
name: "Dong"
}];
let arr1 = [1, 2, 3];
let arr2 = [4, 5];
let valueN = [6, 7];
let new_array = old_array.concat(arr1, arr2, ...valueN)
old_array[1].name = "Hui";
console.log(old_array); // [ 'Hui', { name: 'Hui' } ]
console.log(new_array);
// [ 'Hui', { name: 'Dong' }, 1, 2, 3, 4, 5, 6, 7 ]
2.5. Array.from()
从一个类数组或可迭代对象创建一个新的,浅拷贝的数组实例。
console.log(Array.from('foo'));
// [ 'f', 'o', 'o' ]
console.log(Array.from(('Tian'), x => x + "1"));
// [ 'T1', 'i1', 'a1', 'n1' ]
const set = new Set(['foo', 'bar', 'baz', 'foo']);
console.log(Array.from(set));
// [ 'foo', 'bar', 'baz' ]
const map = new Map([[1, 2], [2, 4], [4, 8]]);
console.log(Array.from(map));
// [[1, 2], [2, 4], [4, 8]]
const mapper = new Map([['1', 'a'], ['2', 'b']]);
console.log(Array.from(mapper.values()));
// ['a', 'b'];
console.log(Array.from(mapper.keys()));
// ['1', '2'];
2.6. 实现浅拷贝
代码参考 kaiwu.lagou.com/course/cour…
const shallowCopy = (target) => {
if (typeof target === 'object' && target !== null) {
const cloneTarget = Array.isArray(target) ? [] : {};
for (let prop in target) {
if (target.hasOwnProperty(prop)) {
cloneTarget[prop] = target[prop];
}
}
return cloneTarget;
} else {
return target;
}
}
3. deepClone
3.1. JSON
JSON.stringify() (序列化)和 JSON.parse() (解析)。
old_obj = {
a: 0,
b: {
c: 0
}
}
let new_obj = JSON.parse(JSON.stringify(old_obj));
old_obj.a = 4;
old_obj.b.c = 4;
console.log(old_obj); // { a: 4, b: { c: 4 } }
console.log(new_obj); // { a: 0, b: { c: 0 } }
3.2. 递归实现
两个方法,第二个源于yeyan1996的博客
function deepClone (origin, target1) {
let target2 = target1 || {};
for (let prop in origin) {
if (origin.hasOwnProperty(prop)) {
if (origin[prop] !== 'null' && typeof (origin[prop]) == 'object') {
if (
Object.prototype.toString.call(origin[prop]) == '[object Array]'
) {
target2[prop] = [];
} else {
target2[prop] = {};
}
deepClone(origin[prop], target2[prop]);
} else {
target2[prop] = origin[prop];
}
}
}
return target2;
}
注意其中传target时,必须是个object型的数据。
3.3. jQuery、lodash
-
jQuery.extend():将两个或者更多个对象的内容合并到第一个对象
3.4. MessageChannel
参考:
- jessezhao1990→JavaScript 深拷贝
- MessageChannel 返回一个带有两个MessagePort属性的MessageChannel新对象。这在node不可用,在 Web Worker 中可用。
function structuralClone(obj) {
return new Promise(resolve => {
const {
port1,
port2
} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
})
}
let obj = {
per: {
name: '田甜'
},
fun: ['HFUT', '电信']
};
structuralClone(obj).then(res => {
obj.per.name = "哈哈";
obj.fun[1] = "YAMA";
console.log(res);
})
console.log(obj);
4. 关于 Setter
当尝试设置属性时,set语法将对象属性绑定到要调用的函数。
- get一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。默认为 undefined。
- set一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。默认为 undefined
表达式:从 ES6 开始,还可以使用一个计算属性名的表达式绑定到给定的函数。
const language = {
set current (name) { //我认为current就像一个函数,或者说language的一个方法
this.log.push(name);
console.log(name);
},
log: [],
};
language.current = "EN";
language.current = "FA";
console.log(language.log); //["EN", "FA"]
5. 附加
- map() 方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
- 箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this,arguments,super或new.target。箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。在箭头函数中this的值取决于它的上一层非箭头函数的
- 三目运算:
console.log(1 > 0 ? ("10" > "9" ? 1 : 0) : 2); // 输出0。数字字符串与数字比会转换为数字,但是字符串与字符串比会从左到右逐位相比,首先1就不大于9,所以输出0,再举个例子:"21">"199"//输出true因为2先和1比直接就true了。结合三目运算可以简化上述自己实现的克隆方法,如果把Object.prototype.toString这样的方法用变量来代替会更加简洁。
function deepClone (origin, target) {
var target = target || {};
for (var prop in origin) {
if (origin.hasOwnProperty(prop)) {
origin[prop] !== 'null' && typeof (origin[prop]) == 'object' ? (Object.prototype.toString.call(
origin[prop]) ==
'[object Array]' ? target[prop] = [] : target[prop] = {}, deepClone(origin[prop], target[
prop])) : target[
prop] = origin[prop];
}
}
return target;
}
6. Clone总结
基本数据类型没有深浅拷贝拷贝之分,对于引用数据类型,浅拷贝指的是拷贝引用,所以拷贝之后会有两个对象同时指向一个内存。
深拷贝则是完全复制其相应的键和值,拷贝之后两个object就没有了联系。文中给出了部分方法并且考虑不全,对于一些特殊的场景还需重新考虑。