前面系列即为前端面试系列(Front-end interview series), 主要内容是一些前端面试中经常被问到的题.
系列问答中没有繁琐的讲解过程, 力求保证面试者给予面试官一个简洁、具有重点的答案, 所以适合于有一定知识基础的前端童鞋👨🎓. 当然, 在每题的最后我也会贴上关于这一章节比较好文章, 以供大家更好的理解所提到的知识点.
请认准github地址: niubility-coding-js
Object.assign()
方法- 展开算法
Spread
({...obj}
) - 操作数组的方法
slice()、concat()
- 比较流氓式的方式:
JSON.parse(JSON.stringify(object))
- 自己实现一个深拷贝的方法
- 使用一些工具库里的方法, 比如
lodash
里的cloneDeep()
,jquery
里的extend()
.
- 会忽略属性值为
undefined
、symbol
、函数的这三种情况, 也就是不进行拷贝 - 不能解决循环引用问题
- 不能正确的处理
new Date
- 不能处理正则(为正则的时候, 拷贝过去为一个空对象
{}
)
思路:
- 判断原生
Object
是否支持assign
, 若是不存在的话则使用Object.defineProperty
方法将该函数绑定到Object
中 - 判断传入的目标参数是否正确, 是否为
null
- 使用
Object()
将目标参数转为对象, 并保存为to
, 最终要返回这个to
- 使用
for..in
循环遍历出所有可枚举的自有属性。并复制给新的目标对象(使用hasOwnProperty
获取自有属性,即非原型链上的属性) - 在设计的时候需要在让该方法在严格模式下进行
function createAssign2 () {
if (typeof Object.assign2 !== 'function') {
Object.defineProperty(Object, 'assign2', {
value: function (target) {
"use strict";
if (target == null) {
throw new Error("Cannot convert undefined or null to object")
}
var to = Object(target);
for (var i = 1; i < arguments.length; i++) {
var nextSource = arguments[i];
for (var nextKey in nextSource) {
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
return to;
},
writable: true,
configurable: true
})
}
}
createAssign2();
空
白
格
function createAssign2() {
// 1. 判断是否存在assign2
if (typeof Object.assign2 !== 'function') {
// 2. 给 Object 中添加属性
Object.defineProperty(Object, 'assign2', {
value: function(target) {
// 4. 开启严格模式
"use strict";
// 5. 判断目标对象是否为 null
if (target == null) {
throw new Error("Cannot convert undefined or null to object")
}
// 6. 使用 Object() 包裹
var to = Object(target);
// 7. 遍历后面传入的所有对象
for (var i = 1; i < arguments.length; i++) {
var nextSource = arguments[i];
// 8. 使用 for...in 查找所有的属性(包括原型链中的属性)
for (var nextKey in nextSource) {
// 9. 只考虑自身的属性
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
// 10. 返回to
return to;
},
writable: true,
configurable: true
})
}
}
createAssign2();
注:
- 为什么要在严格模式下 🤔️?
那是因为使用原生的Object.assign()
时, 若是有以下情况:
var newObj = Object.assign('123', '456', { name: 'LinDaiDai' });
// or
var newObj = Object.assign('123', { 0: '4' });
上面👆两种情况都会报错:
Cannot assign to read only property '0' of object '[object String]'
但如果是下面👇的情况就不会:
var newObj = Object.assign(123, '456'); // Number{0:"4", 1:"5", 2:"6" }
// or
var newObj = Object.assign('123', 456) // String{0: "1", 1: "2", 2: "3"}
看出什么了吗? 😊
如果参数中同时存在两个字符串, 比如"123"和"456"
就会报错, 或者存在一个字符串和一个带有0
属性的对象, 也会报错.
那是因为字符串类型的数据的可写属性writable是false
:
var str = 'abc';
console.log(Object.getOwnPropertyDescriptor(str, "0"));
// {
// value: 'a',
// writable: false, // 可写属性
// enumerable: true,
// configurable: false
// }
而平常我们使用下标修改的方式直接修改是不会报错的, 只会修改不成功:
var str = "abc";
str[0] = "d";
console.log(str[0]); // "a" 还是"a", 证明没有修改成功
但是在严格模式下就会报错了:
"use strict";
var str = "abc";
str[0] = "d";
// Cannot assign to read only property '0' of object '[object String]'
换句话说: 在严格模式下, 如果你试图修改一个writable: false
的属性的时候就会报错.
所以若是assign()
方法的第一个参数的数据类型是非空的字符串, 而后面也有非空的字符串或者带有0
属性的对象时, 就会报错, 因此我们在设计assign2
的时候, 需要在严格模式下进行.
- 为什么需要有
var to = Object(target)
这一步 🤔️?
因为如果第一个参数target
为基本数据类型(string、number、symbol
)时, 需要将其包装为对象, 如果没有Object(target)
这一步的话, 就会报错了:
Uncaught TypeError: Cannot create property 'obj' on string '123'
而如果第一个参数是这种基本数据类型的时候, 生成的对象会是这样:
var newObj = Object.assign("123", { name: "LinDaiDai" });
// String{ 0: "1", 1: "2", 2: "3", name: "LinDaiDai"}
前面的String
表示新生成的对象的__proto__
为String
对象(而非Object
对象), 而且其中有一个名为[[PrimitiveValue]]
的属性为"123"
.
测试代码:
createAssign2();
var obj = {
name: 'obj',
colors: ['white', 'black']
}
var obj2 = Object.assign2({}, obj, { sex: 'boy' })
obj2.name = 'obj2'
obj2.colors.push('yellow')
console.log(obj) // { name: 'obj', colors: ['white', 'black', 'yellow'], sex: 'boy' }
console.log(obj2) // { name: 'obj2', colors: ['white', 'black', 'yellow'], sex: 'boy' }
思路:
- 对传入的参数做校验, 不是对象类型则返回参数本身
- 校验的方式, 可以使用
Object.prototype.toString.call(obj)
的方式来判断 - 需要考虑是数组还是对象
- 需要考虑循环引用问题(可以用
WeakMap
或者数组来解决) - 需要判断每一个属性的类型, 若是
Object
和Array
的时候, 要进行递归处理
function isObject (object) {
let type = Object.prototype.toString.call(object).slice(8, -1);
return type === 'Object' || type === 'Array';
}
function cloneDeep (source, hash = new WeakMap()) {
if (!isObject(source)) return source
if (hash.has(source)) return hash.get(source);
var target = Array.isArray(source) ? [...source] : {...source};
hash.set(source, target);
Reflect.ownKeys(target).forEach(key => {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash);
} else {
target[key] = source[key];
}
})
return target;
}
空
白
格
过程分析:
// 判断类型是不是对象或者数组
function isObject(object) {
let type = Object.prototype.toString.call(object).slice(8, -1);
return type === 'Object' || type === 'Array';
}
function cloneDeep(source, hash = new WeakMap()) {
// 1. 判断不是数组或者对象的时候则返回其本身
if (!isObject(source)) return source
// 2. 通过WeakMap解决循环引用问题
if (hash.has(source)) return hash.get(source);
// 3. 判断是否是数组
var target = Array.isArray(source) ? [...source] : {...source};
hash.set(source, target);
// 4. 利用`Reflect.ownKeys()`获取target本身的属性(非原型链上的属性), 且包含了symbol类型的属性
Reflect.ownKeys(target).forEach(key => {
// 5. 再次判断是否是对象类型
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash);
} else {
target[key] = source[key];
}
})
// 6. 返回target
return target;
}
测试代码:
var obj = {
a: undefined,
b: null,
c: Symbol('c'),
d: function() {
console.log(d)
},
e: new Date(),
f: /^123/,
g: {
name: 'objName'
}
}
obj[Symbol("h")] = "localH"; // symbol类型
obj.a = obj.g; // 循环引用
obj.g.sex = obj.a
var newObj = cloneDeep(obj);
obj.g.name = 'modifyName';
console.log(obj)
console.log(newObj)
注:
- 为什么判断是否是对象需要这样写 🤔️?
这里我判断是否是对象是通过Object.prototype.toString.call(object)
的方式,
我看有些教材是采用:
function isObject (object) {
return typeof object === 'object' && object !== null;
}
但是如果属性值是Date、RegExp
的时候, 也会被判断为true
了, 这样就走了递归路线, 导致这种类型的值拷贝失败...
- 为什么遍历属性要这样写 🤔️?
这里我遍历属性的方式是:
Reflect.ownKeys(target).forEach(key => {})
采用此方式的优点是:
- 只遍历元素自身的属性, 不会遍历原型链上的
- 可以遍历出类型为
symbol
类型的属性 - 它与
Object.keys()
相似, 但是它不会受enumerable
影响
- 使用
for...in...
遍历属性有什么问题 🤔️?
采用for...in...
的形式的话其实也可以, 不过你得多做几个处理了, 因为for...in...
的形式有以下两个问题:
- 会把原型链上的属性也遍历出来, 因此你得这样判断:
for (var key in target) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
// 这里就过滤掉了原型链上的属性
}
}
- 遍历不了
symbol
类型的属性
如果你想要获取类型为symbol
的属性可以采用下面这个方法:
var obj = {};
obj[Symbol("h")] = "localH";
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(h)]
而我这里用的Reflect.ownKeys()
的方法其实就等同于下面👇的写法:
Object.getOwnPropertyNames(obj).concat(Object.getOwnPropertySymbols(obj));
此时兼容的写法就是:
// 这里获取到了所有symbol类型的属性
var symbolKeys = Object.getOwnPropertySymbols(target);
if (symbolKeys.length > 0) {
symBolKeys.forEach(symKey = > {
if (isObject(source[symKey])) {
target[symKey] = cloneDeep(source[symKey], hash);
} else {
target[symKey] = source[symKey];
}
})
}
// 下面这个 for...in... 是遍历非symbol类型的属性
for (var key in target) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash);
} else {
target[key] = source[key];
}
}
}
这样其实也可以, 不过感觉没有必要...
测试代码:
var obj = {
name: 'obj',
colors: ['white', 'black'],
persons: [{ name: 'p1' }, { name: 'p2' }]
}
var obj2 = cloneDeep(obj)
obj2.name = 'obj2'
obj2.colors.push('yellow')
console.log(obj) // { name: 'obj', colors: ['white', 'black'], persons: [...] }
console.log(obj2) // { name: 'obj2', colors: ['white', 'black', 'yellow'], persons: [...] }
问题产生原因:
const source = {
set foo (value) {
console.log(value)
},
get bar () {
return 'LinDaiDai'
}
}
const target1 = {}
Object.assign(target1, source)
console.log(Object.getOwnPropertyDescriptor(target1, 'foo'))
结果为:
{value: undefined, writable: true, enumerable: true, configurable: true}
这里的foo
是一个set
属性,但是在进行Object.assign()
调用的时候确不能正确的拷贝,也就是获取到的value
是为undefined
。
解决办法:
可以使用ES8
中的getOwnPropertyDescriptors()
配合Object.defineProperty()
:
const source = {
set foo (value) {
console.log(value)
},
get bar () {
return 'LinDaiDai'
}
}
const target1 = {}
// Object.assign(target1, source)
// console.log(Object.getOwnPropertyDescriptor(target1, 'foo'))
Object.defineProperties(target1, Object.getOwnPropertyDescriptors(source))
console.log(Object.getOwnPropertyDescriptor(target1, 'foo'))
结果为:
{get: undefined, enumerable: true, configurable: true, set: ƒ}
知识无价, 支持原创
参数文章: