文章目录
原型和原型链
什么是原型?
- 每个对象都有_proto_属性,并且指向它的原型对象
- 每个构造函数都有它的prototype原型对象
prototype原型对象里的constructor指向它的构造函数
new一个构造函数会形成它的实例对象
原型的作用:
- 数据共享 节约内存内存空间
- 实现继承
原型链
当查找一个对象的某个属性时,会先从它自身的属性上查找,如果找不到的话会从它的_proto_属性上查找,就是这个构造函数的prototype属性,如果还没找到就会继续在_proto_上查找,直到最顶层,找不到则为undefined,像这样一层一层去查找形成一个链式的称为原型链
原型和原型链介绍
执行上下文与执行上下文栈
执行上下文是当前JavaScript代码被解析和执行时所在环境
全局执行上下文
- 在执行全局代码前将window确定为全局执行上下文
- 对全局数据进行预处理
- var定义的全局变量==>undefined, 添加为window的属性
- function声明的全局函数==>赋值(fun), 添加为window的方法
- this==>赋值(window)
- 开始执行全局代码
函数执行上下文
- 在调用函数, 准备执行函数体之前, 创建对应的函数执行上下文对象(虚拟的, 存在于栈中)
- 对局部数据进行预处理
- 形参变量==>赋值(实参)==>添加为执行上下文的属性
- arguments==>赋值(实参列表), 添加为执行上下文的属性
- var定义的局部变量==>undefined, 添加为执行上下文的属性
- function声明的函数 ==>赋值(fun), 添加为执行上下文的方法
- this==>赋值(调用函数的对象)
- 变量提升与函数提升
- 变量提升: 在变量定义语句之前, 就可以访问到这个变量(undefined)
- 函数提升: 在函数定义语句之前, 就执行该函数
- 先有变量提升, 再有函数提升
- 理解
- 执行上下文: 由js引擎自动创建的对象, 包含对应作用域中的所有变量属性
- 执行上下文栈: 用来管理产生的多个执行上下文
- 分类:
- 全局: window
- 函数: 对程序员来说是透明的
- 生命周期
- 全局 : 准备执行全局代码前产生, 当页面刷新/关闭页面时死亡
- 函数 : 调用函数时产生, 函数执行完时死亡
- 包含哪些属性:
- 全局 :
- 用var定义的全局变量 ==>undefined
- 使用function声明的函数 ===>function
- this ===>window
- 函数
- 用var定义的局部变量 ==>undefined
- 使用function声明的函数 ===>function
- this ===> 调用函数的对象, 如果没有指定就是window
- 形参变量 ===>对应实参值
- arguments ===>实参列表的伪数组
- 全局 :
执行上下文栈
每个函数都有自己的执行环境,当执行流进入一个函数时,函数的环境就会被推入一个环境栈中。函数执行完后,栈将其环境弹出,把控制权返回给之前的执行环境。
一个程序代码中包含多个函数,也就是包含多个函数执行上下文,为了管理好多个执行上下文之间的关系,JavaScript中创建了执行上下文栈来管理执行上下文。执行上下文栈是具有后进先出结构的栈结构,用于存储在代码执行期间创建的所有执行上下文。
当JavaScript引擎运行JavaScript代码时它会创建一个全局执行上下文并将其push到当前调用栈。(函数还没解析或者是执行、调用)仅存在全局执行上下文,每当引擎发现函数调用时,引擎都会为该函数创建一个新的函数执行上下文,并将其推入到堆栈的顶部(当前执行栈的栈顶)。当引擎执行其执行上下文位于堆栈顶部的函数之后,将其对应的函数执行上下文将会从堆栈中弹出,并且控件到达当前堆栈中位于其下方的上下文(如果有下一个函数的话)
- 执行上下文创建和初始化的过程
- 全局:
- 在全局代码执行前最先创建一个全局执行上下文(window)
- 收集一些全局变量, 并初始化
- 将这些变量设置为window的属性
- 函数:
- 在调用函数时, 在执行函数体之前先创建一个函数执行上下文
- 收集一些局部变量, 并初始化
- 将这些变量设置为执行上下文的属性
- 全局:
作用域与作用域链
- 理解:
- 作用域: 一块代码区域, 在编码时就确定了, 不会再变化
- 作用域链: 多个嵌套的作用域形成的由内向外的结构, 用于查找变量,
作用域链的作用是保证执行环境里有权访问的变量和函数是有序的,作用域链的变量只能向上访问,变量访问到window对象即被终止,作用域链向下访问变量是不被允许的。
- 分类:
- 全局
- 函数
- js没有块作用域(在ES6之前)
- 作用
- 作用域: 隔离变量, 可以在不同作用域定义同名的变量不冲突
- 作用域链: 查找变量
- 区别作用域与执行上下文
- 作用域: 静态的, 编码时就确定了(不是在运行时), 一旦确定就不会变化了
- 执行上下文: 动态的, 执行代码时动态创建, 当执行结束消失
- 联系: 执行上下文环境是在对应的作用域中的
闭包
- 理解:
- 当嵌套的内部函数引用了外部函数的变量时就产生了闭包
- 通过chrome工具得知: 闭包本质是内部函数中的一个对象, 这个对象中包含引用的变量属性
- 作用:
- 延长局部变量的生命周期
- 让函数外部能操作内部的局部变量
- 写一个闭包程序
function fn1() { var a = 2; function fn2() { a++; console.log(a); } return fn2; } var f = fn1(); f(); f();
- 闭包应用:
- 模块化: 封装一些数据以及操作数据的函数, 向外暴露一些行为
- 循环遍历加监听
- JS框架(jQuery)大量使用了闭包
- 缺点:
- 变量占用内存的时间可能会过长
- 可能导致内存泄露
- 解决:
- 及时释放 : f = null; //让内部函数对象成为垃圾对象
内存溢出与内存泄露
- 内存溢出
- 一种程序运行出现的错误
- 当程序运行需要的内存超过了剩余的内存时, 就出抛出内存溢出的错误
- 内存泄露
- 占用的内存没有及时释放
- 内存泄露积累多了就容易导致内存溢出
- 常见的内存泄露:
- 意外的全局变量
- 没有及时清理的计时器或回调函数
- 闭包
js判断类型
1、typeof检测不出null 和 数组,结果都为object,所以typeof常用于检测基本类型
null 有属于自己的类型 Null,而不属于Object类型,typeof 之所以会判定为 Object 类型,是因为JavaScript 数据类型在底层都是以二进制的形式表示的,二进制的前三位为 0 会被 typeof 判断为对象类型,而 null 的二进制位恰好都是 0 ,因此,null 被误判断为 Object 类型。
2、instanceof不能检测出number、boolean、string、undefined、null、symbol类型,所以instancof常用于检测复杂类型以及级成关系
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
3、constructor
null、undefined没有construstor方法,因此constructor不能判断undefined和null。但是contructor的指向是可以被改变,所以不安全
4、Object.prototype.toString.call全类型都可以判断,但是不能准确地判断他是哪一个类的实例,不能用对象本身的toString()方法,因为都被重写了
手写instanceof
function myInstanceof (left, right) {
// 基本数据类型直接返回false
if (typeof left !== 'object' || left === null) return false
// getProtypeOf是Object对象自带的一个方法,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left)
while (true) {
// 查找到尽头,还没找到
if (proto == null) return false
// 找到相同的原型对象
if (proto == right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
}
测试
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true
浅谈Object.prototype.toString.call()方法
- 在JavaScript里使用typeof判断数据类型,只能区分基本类型,即:number、string、undefined、boolean、object。
- 对于null、array、function、object来说,使用typeof都会统一返回object字符串。
要想区分对象、数组、函数、单纯使用typeof是不行的。在JS中,可以通过Object.prototype.toString方法,判断某个对象之属于哪种内置类型。分为null、string、boolean、number、undefined、array、function、object、date、math。
1. 判断基本类型
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
2. 判断原生引用类型
**函数类型**
Function fn(){
console.log(“test”);
}
Object.prototype.toString.call(fn); // "[object Function]"
**日期类型**
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
**数组类型**
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"
**正则表达式**
var reg = /[hbc]at/gi;
Object.prototype.toString.call(reg); // "[object RegExp]"
**自定义类型**
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person("Rose", 18);
Object.prototype.toString.call(arr); // "[object Object]"
很明显这种方法不能准确判断person是Person类的实例,而只能用instanceof 操作符来进行判断,如下所示:
console.log(person instanceof Person); // true
3. 判断原生JSON对象
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON);
console.log(isNativeJSON);// 输出结果为”[object JSON]”说明JSON是原生的,否则不是;
注意:Object.prototype.toString()本身是允许被修改的,而我们目前所讨论的关于Object.prototype.toString()这个方法的应用都是假设toString()方法未被修改为前提的。
undefined 和 null 区别
- null
什么都没有,表示一个空对象引用(主动释放一个变量引用的兑现那个,表示一个变量不再指向任何引用地址) - undefined
没有设置值的变量,会自动赋值undefined - 区别
typeof undefined // undefined
typeof null // object
null === undefined // false
null == undefined // true
普通函数和箭头函数的区别
- 普通函数
可以通过bind、call、apply改变this指向
可以使用new - 箭头函数
- 本身没有this指向
- 它的this在定义的时候继承自外层第一个普通函数的this
- 被继承的普通函数的this指向改变,箭头函数的this指向会跟着改变
- 箭头函数外层没有普通函数时,this指向window
- 不能通过bind、call、apply改变this指向
- 使用new调用箭头函数会报错,因为箭头函数没有constructor
this指向问题
document.write和innerHTML的区别
document.write 将内容写入页面,清空替换掉原来的内容,会导致重绘
document.innerHTML 将内容写入某个Dom节点,不会重绘
栈和堆的区别
-
堆
动态分配内存,内存大小不一,也不会自动释放 -
栈
自动分配相对固定大小的内存空间,并由系统自动释放 -
基本类型都是存储在栈中,每种类型的数据占用的空间的大小是确定的,并由系统自动分配和释放。内存可以及时回收。
-
引用类型的数据都是存储在堆中。准确说是栈中会存储这些数据的地址指针,并指向堆中的具体数据。
JS哪些操作会造成内存泄露
内存泄漏是指一块被分配的内存既不能使用,也不能回收,直到浏览器进程结束。
1、意外的全局变量
2、闭包
3、没有清理的dom元素
dom元素赋值给变量,又通过removeChild移除dom元素。但是dom元素的引用还在内存中
4、被遗忘的定时器或者回调
谈谈垃圾回收机制方式及内存管理
JavaScript 在定义变量时就完成了内存分配。当不在使用变量了就会被回收,因为其开销比较大,垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
- 垃圾回收
- 标记清除法
当变量进入环境时,将这个变量标记为’进入环境’。当标记离开环境时,标记为‘离开环境’。离开环境的变量会被回收 - 引用技计数法
跟踪记录每个值被引用的次数,如果没有被引用,就会回收
- 内存管理
内存分配=》内存使用=》内存回收
JS中的执行机制(setTimeout、setInterval、promise、宏任务、微任务)
1、执行机制
JS 是单线程的,处理 JS 任务(程序)只能一个一个顺序执行,所以 JS 中就把任务分为了同步任务和异步任务。同步的进入主线程先执行,异步的进入Event Table并注册函数,当指定的事情完成时,Event Table会将这个函数移入事件队列Event Queue,等待主线程内的任务执行完毕,然后就会从事件队列 Event Queue 中读取对应的函数,进入主线程执行。
除了广义的同步任务和异步任务,JS 对任务还有更精细的定义:
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval
- micro-task(微任务):Promise,process.nextTick
微任务先于宏任务执行(除了一开始的整体代码 script)。执行过程中,不同类型的任务会进入对应的事件队列Event Queue,比如setTimeout和setInterval会进入相同的Event Queue。
1.1、执行优先级
- 同步代码执行顺序优先级高于异步代码执行顺序优先级
- process.nextTick() > Promise.then() > setTimeout > setImmediate
(注意:process.nextTick 是 node 中的方法,而在浏览器中执行时(比如在vue项目中),会退化成setTimeout,所以在浏览器中 process.nextTick 会比 Promise.then() 慢)
1.2、总结
总得来说,在 JS 中,先是执行整体的同步任务代码,遇到微任务就会将其放在微任务事件队列,遇到宏任务就会放在宏任务事件队列中。
然后整体的同步任务代码执行完后,就会先执行微任务队列中的任务,等待微任务队列中的所有任务执行完毕后,此时才会从宏任务队列中找到第一个任务进行执行。该任务执行过程中,如果遇到微任务就会放到微任务队列中,等到该任务执行完后,就会查看微任务队列中有没有微任务,如果有就先执行完微队列中的任务,否则执行第二个宏任务。以此类推。
为什么JavaScript是单线程?
javaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完
全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
为什么JavaScript是单线程
js事件循环机制
如上图为事件循环示例图(或JS运行机制图),流程如下:
-
step1:主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;
-
step2: 主线程遇到异步任务,指给对应的异步进程进行处理(WEB API);
-
step3: 异步进程处理完毕(Ajax返回、DOM事件处罚、Timer到等),将相应的异步任务推入任务队列;
-
step4: 主线程执行完毕,查询任务队列,如果存在任务,则取出一个任务推入主线程处理(先进先出);
-
step5: 重复执行step2、3、4;称为事件循环。
执行的大意:
同步环境执行(step1) -> 事件循环1(step4) -> 事件循环2(step4的重复)…
其中的异步进程有:
-
a、类似onclick等,由浏览器内核的DOM binding模块处理,事件触发时,回调函数添加到任务队列中;
-
b、setTimeout等,由浏览器内核的Timer模块处理,时间到达时,回调函数添加到任务队列中;
-
c、Ajax,由浏览器内核的Network模块处理,网络请求返回后,添加到任务队列中。
JS创建对象的6种方式总结
一、new 操作符 + Object 创建对象
var person = new Object();
person.name = "lisi";
person.age = 21;
person.family = ["lida","lier","wangwu"];
person.say = function(){
alert(this.name);
}
二、字面式创建对象
var person ={
name: "lisi",
age: 21,
family: ["lida","lier","wangwu"],
say: function(){
alert(this.name);
}
};
以上两种方法在使用同一接口创建多个对象时,会产生大量重复代码,为了解决此问题,工厂模式被开发。
三、工厂模式
function createPerson(name,age,family) {
var o = new Object();
o.name = name;
o.age = age;
o.family = family;
o.say = function(){
alert(this.name);
}
return o;
}
var person1 = createPerson("lisi",21,["lida","lier","wangwu"]); //instanceof无法判断它是谁的实例,只能判断他是对象,构造函数都可以判断出
var person2 = createPerson("wangwu",18,["lida","lier","lisi"]);console.log(person1 instanceof Object); //true
工厂模式解决了重复实例化多个对象的问题,但没有解决对象识别的问题(但是工厂模式却无从识别对象的类型,因为全部都是Object,不像Date、Array等,本例中,得到的都是o对象,对象的类型都是Object,因此出现了构造函数模式)。
四、构造函数模式
function Person(name,age,family) {
this.name = name;
this.age = age;
this.family = family;
this.say = function(){
alert(this.name);
}
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
var person2 = new Person("lisi",21,["lida","lier","lisi"]);
console.log(person1 instanceof Object); //true
console.log(person1 instanceof Person); //true
console.log(person2 instanceof Object); //true
console.log(person2 instanceof Person); //trueconsole.log(person1.constructor); //constructor 属性返回对创建此对象的数组、函数的引用
对比工厂模式有以下不同之处:
1、没有显式地创建对象
2、直接将属性和方法赋给了 this 对象
3、没有 return 语句
以此方法调用构造函数步骤 {
1、创建一个新对象
2、将构造函数的作用域赋给新对象(将this指向这个新对象)
3、执行构造函数代码(为这个新对象添加属性)
4、返回新对象 ( 指针赋给变量person ??? )
}
可以看出,构造函数知道自己从哪里来(通过 instanceof 可以看出其既是Object的实例,又是Person的实例)
构造函数也有其缺陷,每个实例都包含不同的Function实例( 构造函数内的方法在做同一件事,但是实例化后却产生了不同的对象,方法是函数 ,函数也是对象)详情见构造函数详解
因此产生了原型模式
五、原型模式
function Person() {
}
Person.prototype.name = "lisi";
Person.prototype.age = 21;
Person.prototype.family = ["lida","lier","wangwu"];
Person.prototype.say = function(){
alert(this.name);
};
console.log(Person.prototype); //Object{name: 'lisi', age: 21, family: Array[3]}
var person1 = new Person(); //创建一个实例person1
console.log(person1.name); //lisi
var person2 = new Person(); //创建实例person2
person2.name = "wangwu";
person2.family = ["lida","lier","lisi"];
console.log(person2); //Person {name: "wangwu", family: Array[3]}
// console.log(person2.prototype.name); //报错
console.log(person2.age); //21
原型模式的好处是所有对象实例共享它的属性和方法(即所谓的共有属性),此外还可以如代码第16,17行那样设置实例自己的属性(方法)(即所谓的私有属性),可以覆盖原型对象上的同名属性(方法)。具体参见原型模式详解
六、混合模式(构造函数模式+原型模式)
function Person(name,age,family){
this.name = name;
this.age = age;
this.family = family;
}
Person.prototype = {
constructor: Person, //每个函数都有prototype属性,指向该函数原型对象,原型对象都有constructor属性,这是一个指向prototype属性所在函数的指针
say: function(){
alert(this.name);
}
}
var person1 = new Person("lisi",21,["lida","lier","wangwu"]);
console.log(person1);
var person2 = new Person("wangwu",21,["lida","lier","lisi"]);
console.log(person2);
可以看出,混合模式共享着对相同方法的引用,又保证了每个实例有自己的私有属性。最大限度的节省了内存
继承
1.借助构造函数实现继承(部分继承)
/**
* 借助构造函数实现继承
*/
function Parent1() {
this.name = 'parent';
}
Parent1.prototype.say = function() {}; // 不会被继承
function Child1() {
// 继承:子类的构造函数里执行父级构造函数
// 也可以用apply
// parent的属性都会挂载到child实例上去
// 借助构造函数实现继承的缺点:①如果parent1除了构造函数里的内容,还有自己原型链上的东西,自己原型链上的东西不会被child1继承
// 任何一个函数都有prototype属性,但当它是构造函数的时候,才能起到作用(构造函数是有自己的原型链的)
Parent1.call(this);
this.type = 'child1';
}
console.log(new Child1);
(1)如果父类的属性都在构造函数内,就会被子类继承。
(2)如果父类的原型对象上有方法,子类不会被继承。
2:借助原型链实现继承
/**
* 借助原型链实现继承
*/
function Parent2() {
this.name = 'name';
this.play = [1, 2, 3]
}
function Child2() {
this.type = 'child2';
}
Child2.prototype = new Parent2(); // prototype使这个构造函数的实例能访问到原型对象上
console.log(new Child2().__proto__);
console.log(new Child2().__proto__ === Child2.prototype); // true
var s1 = new Child2(); // 实例
var s2 = new Child2();
console.log(s1.play, s2.play);
s1.play.push(4);
console.log(s1.__proto__ === s2.__proto__); // true // 父类的原型对象
(1)原型链的基本原理:构造函数的实例能访问到它的原型对象上
(2)缺点:原型链中的原型对象,是共用的
3:组合方式
第一种
/**
* 组合方式
*/
function Parent3() {
this.name = 'name';
this.play = [1, 2, 3];
}
function Child3() {
Parent3.call(this);
this.type = 'child3';
}
Child3.prototype = new Parent3();
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);
// 父类的构造函数执行了2次
// 构造函数体会自动执行,子类继承父类的构造函数体的属性和方法
第二种优化
/**
* 组合继承的优化方式1:父类只执行了一次
*/
function Parent4() {
this.name = 'name';
this.play = [1, 2, 3];
}
function Child4() {
Parent4.call(this);
this.type = 'child4';
}
Child4.prototype = Parent4.prototype; // 继承父类的原型对象
var s5 = new Child4();
var s6 = new Child4();
console.log(s5 instanceof Child4, s5 instanceof Parent4); // true
console.log(s5.constructor); // Parent4 //prototype里有个constructor属性,子类和父类的原型对象就是同一个对象, s5的constructor就是父类的constructor
第三种
/**
* 组合继承优化2
*/
function Parent5() {
this.name = 'name';
this.play = [1, 2, 3];
}
function Child5() {
Parent5.call(this);
this.type = 'child5';
}
//可多继承父类实例属性方法,可传递参数;
Child5.prototype = Object.create(Parent5.prototype); // Object.create创建的对象就是参数
Child5.prototype.constructor = Child5;
var s7 = new Child5();
console.log(s7 instanceof Child5, s7 instanceof Parent5);
console.log(s7.constructor); // 构造函数指向Child5
优点:
可以取到父类实例属性方法,父类原型属性方法;
解决实例共享父类实例属性的问题;
父类构造函数只使用一次;
可多继承父类实例属性方法,可传递参数;
createObject()方式
二、优缺点:
原型链继承的缺点
1、字面量重写原型
一是字面量重写原型会中断关系,使用引用类型的原型,并且子类型还无法给超类型传递参数。
2、借用构造函数(类式继承)
借用构造函数虽然解决了刚才两种问题,但没有原型,则复用无从谈起。所以我们需要原型链+借用构造函数的模式,这种模式称为组合继承
3、组合式继承
组合式继承是比较常用的一种继承方法,其背后的思路是 使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又保证每个实例都有它自己的属性。
为什么使用严格模式:
- 消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的Javascript做好铺垫。
字符串对象转换为json对象
JSON.parse(str)
深拷贝
var deepClone = (obj) => {
var newObj = null; // 初始化对象
if(typeof obj == "object") {
if(Array.isArray(obj)) { // 判断是不是数组
newObj = [];
for(var item of obj) {
newObj.push(deepClone(item));
}
} else if (Object.prototype.toString.call(obj) == "[object Object]") { // 判断是不是对象
newObj = {}; //空对象
for (var key in obj) {
newObj[key] = deepClone(obj[key]);
}
} else { // 其他的就是 比如 函数 正则 null Date Math Set, Map 等js内置对象了等等
newObj = obj;
}
} else { // 不是object类型的比如 string number undefined sympol
newObj = obj;
}
return newObj;
}
var test = { a: 'hello', b: [1, 2]}
var temp = deepClone(test);
test.b[0] = 3;
console.log(temp, test);
JSON.stringify 和 JSON.parse
用 JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象。使用条件是:可以转成 JSON 格式的对象才能使用这种方法,如果对象中包含 function 或 RegExp 这些就不能用这种方法了。
deepCopy = (Obj) => {
let _obj = JSON.stringify(obj);
let objClone = JSON.parse(_obj);
return objClone;
}
o = deepCopy(obj)
o.name = 'cll'
o.color.push('绿色')
console.log(o);
console.log(obj);
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
循环引用
在面试的时候被问到一个问题,再进行深拷贝时遇到循环引用时怎么办?
当时一下子懵了,从俩没有想过这样的问题。
查了一下,解决方法应该是 用一个 Map 来存储引用类型,然后每次遇到引用属性时,就用 has 查看是否已经有了这个引用。
function deepCopy(target, map) {
// typeof 筛选出 obj array null ,前面过滤掉 null
if (!target || typeof target !== "object") {
return null;
}
let result = Array.isArray(target) ? [] : {};
Object.keys(target).forEach((property) => {
if (typeof target[property] !== "object") {
result[property] = target[property];
} else {
//防止循环引用
if (map.has(result[property])) {
result[property] = undefined;
} else {
result[property] = deepCopy(target[property]);
map.set(result[property], true);
}
}
});
return result;
}