前端学习之路-JavaScript高级

95 阅读17分钟

前端学习之路-JavaScript高级

1、函数中this指向问题

  • 默认绑定(指向window)

    • 普通函数的独立调用

      function foo (){
         console.log("foo函数this指向",this);
      }
      foo();
      
    • 函数定义在对象中,但是独立调用

      var obj = {
          bar:function(){
              console.log("bar函数this指向",this);
          }
      }
      var baz = obj.bar;
      baz();
      
    • 严格模式下,独立调用函数中的this指向为undefined

  • 显式绑定

    ​ 不希望对象内部包含这个函数的引用,但又希望在这个对象上强制调用函数时,采用显式绑定.

    • call

    • apply

      function foo (name,address){
          console.log("foo函数this指向",this);
          console.log("打印参数",name,address);
      }
      
      //apply
      //第一个参数:绑定this
      //第二个参数:传入函数需要的实参,以数组的形式
      foo.apply("apply",["Kobe","美国"]);
      
      //call
      //第一个参数:绑定this
      //第二个参数:传入函数需要的实参,以多参数的形式
      foo.call("call","james","美国");
      
    • bind

      function foo(name,age,address){
          console.log("foo函数this指向",this);
          console.log("打印参数:",name,age,address);
      }
         
      var obj = {
          height:2.0
      }
      
      var bar = foo.bind(obj,"kobe",43)
      //参数会自动补全到形参上
      bar("美国");//this -> obj
      
  • 隐式绑定(指向调用对象)

    var obj = {
        bar:function (){
            console.log("bar函数this指向",this);
        }
    }
    obj.bar();
    
  • new绑定

    • 创建一个空对象
    • 将this指向这个空对象
    function foo(){
        console.log("foo函数this指向",this);
    }
    new foo();
    
  • this绑定优先级比较

    • new > bind > apply/call > 隐式绑定 >默认绑定

2、箭头函数

  • 不会绑定this、arguments属性(箭头函数中的this会去上层作用域查找指向,es6之后采用剩余函数代替arguments)
  • 不能作为构造函数使用(不能new)

3、this的面试题

var name = "window";
   function Person(name){
    this.name;
    this.obj = {
      name:'obj',
      foo1: function () {
        return function () {
          console.log(this.name);
        }
      },
      foo2: function () {
        return () => {
          console.log(this.name);
        }
      }
    }
   }

   var person1 = new Person();
   var person2 = new Person();

   person1.obj.foo1()();//window  person1.obj.foo1()返回的值为函数,后面()相当于函数独立运行 
   person1.obj.foo1.call(person2)();//window  person1.obj.foo1.call(person2)返回的值为函数,后面()相当于函数独立运行 
   person1.obj.foo1().call(person2);//person2  person1.obj.foo1()返回值为函数,后面调用call改变this指向为person2

   person1.obj.foo2()();//obj  person1.obj.foo2()返回箭头函数,箭头函数没有this指向,向上层作用域查找,foo2的this指向obj
   person1.obj.foo2.call(person2)();//person2 person1.obj.foo2调用call将this指向person2,person1.obj.foo2.call(person2)的返回值为箭头函数,箭头函数没有this指向,向上层作用域查找即person2
   person1.obj.foo2().call(person2);//obj person1.obj.foo2()的返回值为箭头函数,箭头函数不能通过call改变this指向,向上层作用域查找即obj 
var name = "window";
    var person = {
      name:'person',
      sayName: function () {
        console.log(this.name);
      }
    }

    function sayName(){
      var sss = person.sayName;
      sss(); //window 函数的独立执行即默认绑定
      person.sayName();//person 隐式绑定
      (person.sayName)();//person 隐式绑定
      (b = person.sayName)();//window  间接函数引用,默认绑定
    }
    sayName();

4、浏览器原理

  1. 页面渲染流程

渲染页面流程图.png

1、解析HTML,然后构建DOM Tree。

2、解析过程中遇到link元素会下载css文件(不会影响DOM解析),然后解析CSS文件生成CSSOM Tree。

3、构建出DOM Tree和CSSOM Tree,两者结合构建Render Tree

  • link元素为加载完成会阻塞Render Tree的构建
  • Render Tree和DOM Tree并不是一一对应的,display:none的元素是不会出现在Render Tree中的

4、在Render Tree 上运行layout以计算每个节点的几何体,确定节点的宽高和位置信息

5、将每个节点paint到屏幕上,将元素的可见部分进行绘制,比如文本、颜色、边框、阴影、替换元素。

  1. 回流

    1、回流reflow或者重排:第一次确定节点的大小和位置为layout,之后对节点的大小和位置进行修改成为回流。

    2、引起回流的情况:

    • DOM结构发生改变
    • 布局layout改变
    • 窗口大小改变
    • 调用getComputedStyle方法获取大小和位置信息
  2. 重绘

    1、引起重绘的情况:

    • 修改背景色、文字颜色、边框颜色、样式等
  3. composite合成

    绘制的过程可以将布局后的元素绘制到多个合成层中,每个合成层单独渲染,这是浏览器优化的一种手段,默认情况下,标准流中的内容是被会绘制到同一图层,而一些特殊的属性,创建一个新的合成层.常见的一些属性:

    • 3D transforms
    • video、canvas、iframe
    • opacity 动画转换时
    • position:fixd
    • animation或transition设置了opacity、transform

    分层可以提高性能但是以牺牲内存管理为代价

  4. script元素与页面解析的关系

    在解析HTML过程中,遇到script元素会停止解析HTML并且阻塞构建DOM Tree,然后下载script代码并运行,执行结束后,才会继续解析HTML,继续构建DOM Tree

    • defer属性:不用等待脚本下载,继续解析HTML,构建DOM Tree,脚本下载完成后,等待DOMTree构建完成,在DOMContentloaded事件之前先执行defer中的代码,多个defer的脚本可以保持顺序执行
    • async属性:不会阻塞解析HTML和构建DOM Tree,但是不能保证顺序执行,因为是独立下载和执行的,不会等待其他脚本,而且不会保证在DOMContentLoaded事件之前或者之后执行

5、V8引擎原理

​ V8引擎有C++编写,可以独立运行,也可以嵌入到任何C++的程序中.

V8引擎原理.png

  1. parse模块将代码转换为AST(抽象语法树)
    • 函数没有调用是不会转为AST的
  2. lgnition是解释器,将AST转换为字节码
    • 会收集TurboFan优化所需要的信息
    • 函数只调用一次,Ignition会解释执行字节码
  3. TurboFan是编译器,可以将字节码编译成CPU可直接运行的机器码
    • 一个函数被多次调用,会被标记为热点函数,由TurboFan编译成机器码执行,提高效率
    • 后续执行过程中类型发生转变,那么机器码也会逆向转为字节码

6、JS执行原理

​ 首先会创建一个执行上下文栈,每一个执行上下文会在栈内存中创建一个VO对象与之相关联.当执行上下文为全局时,会在堆内存中创建一个GO对象,此时VO即GO.当执行上下文为函数时,会在堆内存中创建一个AO对象,此时VO即AO

  1. 作用域和作用域链

    • 当进入到一个执行上下文时,执行上下文会关联一个作用域链
    • 作用域链是一个对象列表,用于变量标识符的求值
    var n = 100;
    function foo1(){
        console.log(n);
    }
    function foo2(){
        var n = 200;
        console.log(n);
        foo1();
    }
    foo2();
    //打印结果 200  100
    //关注函数定义的位置而不是调用的位置
    

7、垃圾回收机制

  1. 常见的GC算法

    1、引用计数

    描述:当一个对象有一个引用指向它时,其引用就+1,若引用为0,对象就被销毁。

    缺掉:产生循环引用,占用内存。

    2、标记清除(v8采用)

    描述:设置一个根对象,垃圾回收器会定期从根对象开始查找有引用到的对象,对于没有引用的对象就是不可用对象,进行清除

    3、标记整理

    描述:与标记清楚相似,不同的是,回收期间会将保留下来的对象汇集到连续的内存空间中,避免内存碎片化。

    4、分代收集

    描述:对象被分为新和旧,长期存活的对象被检查的次数会减少

8、闭包

定义:一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包.

9、函数增强

1、函数对象属性

  • name 获取函数名

  • length 获取参数个数(rest参数是不计入参数个数的)

    function demo(m,n){
        ...something
    }
    demo.name;
    demo.length;
    

2、函数中的arguments使用

  1. arguments是类数组对象,用于传递给函数的参数。

    //arguments转数组方式一(ES6)
    Array.form(arguments);
    //方式二(ES6)
    var newArguments = [...arguments]
    //方式三
    var newArguments = [];
    for(var arg of arguments){
        newArguments.push(arg);
    }
    //方式四
    var newArguments = [].slice.apply(arguments);
    
  2. 箭头函数不绑定arguments

  3. 函数的剩余(rest)参数(替代arguments)

function foo (m,n,...args){
    console.log(m,n);
    console.log(args);
}
foo(1,2,3,5,5);
  1. 纯函数

    • 确定的输入,一会产生确定的输出
    • 函数在执行过程中,不能产生副作用(修改全局变量、参数或者外部存储)
  2. 柯里化

    只传递给函数一部分参数来调用它,让他返回一个函数去处理剩余参数,这个过程称之为柯里化,比如:

    //转化前
    f(a,b,c)
    //转化后
    f(a)(b)(c)
    a => b => c=>{}//箭头函数柯里化写法
    
  3. with和eval函数

    //with增加作用域
    var obj = {
        var message = "hello world"
    }
    with(obj){//obj相当于作用域链中新插入的作用域
        console.log(message);
    }
    
    
    //eval函数可以将传入的字符串当做JavaScript代码执行,然后将最后一句执行语句的结果作为返回值
    var evalStr = `var message = "hello world";console.log(message);`
    eval(evalStr);
    
    eval函数的缺点:
    1、可读性差
    2、字符串在执行过程中如果被刻意篡改,容易造成被攻击风险
    3eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化
    
  4. 严格模式

    • 无法意外的创建全局变量
    • 引起静默失败(不报错也没效果)的赋值操作抛出异常
    • 不允许函数参数有相同的名称
    • 不允许使用with
    • eval不再为上层引用变量

10、对象增强

1、描述符

  1. 数据属性描述符

    var obj = {
        name:"kobe",
        age:18
    }
    Object.defineProperty(obj,"name",{
        //属性是否删除
        configurable:true,
        //属性是否可枚举
        enumerable:true,
        // 属性是否能写
        writable:ture
    })
    
  2. 存取属性描述符

    Object.defineProperty(obj,"name",{
        set:function(value){
        //用于监听设置值
        },
        get:function(){
        //用于监听获取值
        }
        //两者默认值都是undefined
    })
    

11、原型(ES5)

1、对象中的原型

var obj = {
    name:"kobe",
    age:18
}
console.log(obj.__proto__);//不建议使用,此调用为浏览器自行实现
console.log(Object.getPrototypeOf(obj));
//需要获取一个属性的值时,会优先在自己的对象中查找,如找到则返回,若无则会在原型中查找

2、函数中的原型

function Foo (){}
//函数作为对象看待时,__proto__此为隐式原型
console.log(Foo.__proto__);
console.log(Object.getPrototypeOf(foo));
//函数作为函数时,prototype此为显式原型
console.log(Foo.prototype);

new Foo()
//首先生成一个空对象
//将函数中的this指向这个空对象
//将函数的prototype(显式原型)赋值给空对象的__proto__(隐式原型)

3、原型中的constructor

function Foo(m,n){
}
var foo1= new Foo(1,2);
//下面两者为同一值
console.log(Foo.prototype.constructor);
console.log(foo1.__proto__.constructor);

console.log(foo.prototype.constructor.name);

函数和对象原型关系.png

4、重写原型对象

​ 创建一个函数,同时创建它的prototype对象,这个对象会自动获取constructor属性,此时的constructor属性默认指向的是Object构造函数,而不是新建的函数。如需更改可通过属性描述符Object.definePrototype()的value属性赋值.

12、原型链(ES5)

1、默认原型链

//{}对象字面量的本质
var obj = {};
//相当于
//var obj = new Object()
//obj.__proto__ === Object.prototype

2、利用原型链实现继承

  1. 方式一:父类的原型直接复制给子类的原型

    • 缺点父类和子类共享一个原型对象,修改其中一个。另外一个也会随之改变
  2. 方式二:创建一个父类的实例化对象,将实例化对象赋值给子类的原型

  3. 方式三:

    function F (){}
    F.prototype = Sup.prototye;
    Sub.prototype = new F();
    
  4. 方式四:寄生组合式继承

    var obj = Object.create(Sup.prototype);
    Sub.prototype = obj;
    //优化并封装为工具函数
    function inherit(subType,supType){
        subType.prototype = Object.create(SupType.prototype);
        Object.definePrototype(subType.prototype,"constructor",{
            enumerable:false,
            configrable:true,
            writable:true,
            value:supType
        });
    }
    

3、利用构造函数继承属性

function Sup(a,b){
    this.a = a;
    this.b = b;
}
function sub(a,b,c){
    Sup.call(this,a,b);
    this.c = c;
}

4、原型继承关系

原型继承关系.png

5、类方法和实例方法

function Foo (){};
//实例方法   只能通过实例化对像调用
Foo.prototype.baz = function(){};
//类方法     可通过类直接调用
Foo.ffo = function(){};

13、class类(ES6)

1、构造方法和实例化

class Foo {
    //通过new关键字创建对象时,会自动调用constructor函数
    constructor(m,n){
        this.m = m;
        this.n = n;
    }
}
var foo1 = new Foo(1,2);
console.log(foo1.m,foo1.n);//1 2
console.log(foo1.__proto__ === Foo.prototype);//true

2、静态方法(类方法)

class Foo{
	//实例方法  只能通过实例化对像调用
    baz(){}
    //静态方法(类方法)  可通过类直接调用
    static ffo(){}
}

3、继承

class Sup{
    constructor(m,n){
        this.m = m;
        this.n = n;
    }
    ffo1(){}
}
class Sub extends Sup{
    constructor(m,n,a,b){
        //this.m = m;
        //this.n = n;
        super(m,n);//在this之前用
        this.a = a;
        this.b = b;
    }
    //ffo1(){}  从父类继承
    ffo2(){}
}

4、对象字面量增强写法

var obj = {
    var name = "kobe";
    var age = 18;
    sayName(){}//即sayName :function(){}但是不能写成箭头函数,this指向会改变
	return {
        name,//name:name
        age  //age:age
    }
}

5、对象和数组的解构

//数组结构 有严格的顺序
var names = ["qqq","ccc","rrrr"];
//方式一 不需要可以逗号空开
var [name1,,name3] = names;
console.log(name1,name3);
//方式二
var [name1,...args] = names;
console.log(name1,args);

//对象解构
var obj = {
    name:"kobe",
    age:18
}
//方式一
var {name,age} = obj;
console.log(name,age);
//方式二 重命名
var {name:newName,age} = obj;
console.log(newName,age);

14、let、const(ES6)

//var 1、重复声明变量2、变量可以作用域提升(可以在声明前访问此变量即作用域提升)

//let const 1、不能重复定义变量且变量不能作用域提升2、const声明的变量不能修改。

//暂时性死区:从块级作用域顶部一直到声明变量完成之前,这块变量区即暂时性死区

15、函数默认参数(ES6)

function foo (m,n){
    //1、之前不严谨的写法
    //方式一
    m = m ? m : "默认值";
    //方式二
    m = m || "默认值";

    //严谨的写法
    // 方式一
    m = (m === undefined || m === null) ? "默认值" : m;
    //方式二(ES6之后新语法优化方式一)
    m = m ?? "默认值";
}

//默认参数简化写法
//有默认参数的形参以及后面的形参不会计入length之内,默认参数的形参最好写在参数最后,若有剩余参数则其写最后.
function foo (m,n = "默认值",...args){
    
}

16、Proxy代理

1、监听对象属性的操作

//方式一(VUE2中响应式数据的基本原理)
//设计弊端:
//1、违反了Object.defineProperty()中的存储数据描述符设计初衷
//2、存在局限性,只能对已有属性进行监听,新增和删除操作无法监听
var obj = {
    name:"cobe",
    age:18,
    address:"美国"
}
const keys = Object.keys(obj);
for (const key of keys) {
    let value  = obj[key];
    Object.defineProperty(obj, key, {
        set:function(newValue){
            console.log(`监听:给${key}设置了新值:${newValue}`);
            value = newValue;
        },
        get:function(){
            console.log(`监听:获取了${key}的值:${value}`);
        }
    })
}
obj.name = "james";
obj.age;
//方式二:使用proxy代理对象,使用proxy对象的set和get捕获器.
const objProxy = new Proxy(obj,{
    set: function (target,key,newValue){
        console.log(`监听:设置了属性${key}的值:${newValue}`);
        target[key] = newValue;
    },
    get: function (target,key){
        console.log(`监听:获取了属性${key}的值:${target[key]}`);
    }
});
objProxy.name = "james";
objProxy.age;
objProxy.tel = 123;//设置新属性

17、Reflect反射

因为ECMA之前对对象设计缺少规范性,将很对API放到了Object上面,造成了Object臃肿,而且一些方法放在其上不在合适,所以新增了Reflect,将一些操作移交至此.

//reflect和proxy结合使用
const objProxy = new Proxy(obj,{
    set: function (target,key,newValue){
        console.log(`监听:设置了属性${key}的值:${newValue}`);
        //target[key] = newValue;
        //采用reflect的方法返回boolean值,在操作失败时可以做判断,并不用再直接操作原对象
        const isSuccess = Reflect.set(target,key,newValue);
        if(!isSuccess){
          throw Error(`设置属性${key}${newValue}失败!`);
        }
    }
});

18、promise异步处理

1、promise的基本实现


//promise制定了异步处理的规范,解决了之前开发中异步处理代码封装复杂和形形色色的现象
function execode (dalay){
    const promise = new Promise((resolve,reject) => {
        setTimeout(() => {
            if(dalay > 0){
                let count = 0;
                for (let i = 0; i < dalay; i++) {
                    count += i;
                }
                resolve(count);
            }else{
                reject(`${dalay}有问题`)
            }
        },3000);
    }); 
    return promise;
}
// const promise = execode(0);
// promise.then(res =>{
//   console.log("执行成功",res)
// });
// promise.catch(rej =>{
//   console.log("执行失败",rej);
// });
execode(100).then(res =>{
    console.log("执行成功",res)
}).catch(rej =>{
    console.log("执行失败",rej);
});

2、promise的三种状态:

  • 待定(pending):初始状态
  • 已兑现(fulfilled):操作成功完成
  • 已拒绝(reject):操作失败

3、resolve

  • 参数为普通值,那么这个值会作为then回调的参数
  • 参数为promise,那么新promise会决定原promise状态
  • 参数为对象并且该对象实现了then方法,执行该then方法,根据该then结果决定promise状态

4、异步处理解决方案(网络请求为例)

//需求:向服务器发送三次网络请求,且上次的请求结果需要用于下次请求参数
function requestData(url){
    return new Promise((resolve,reject) => {
        setTimeout(() => {
            resolve(url);
        },2000);
    });
}

//方式一
//形成回调地域
// function getData(){
//   requestData("aaa").then(res1 =>{
//     requestData(res1+"bbb").then(res2 =>{
//       requestData(res2+"ccc").then(res3 =>{
//         console.log(res3);
//       })
//     })
//   })
// }

//方式二
function getData(){
    requestData("aaa").then(res1 =>{
        return requestData(res1+"bbb");
    }).then(res2 =>{
        return requestData(res2+"ccc");
    }).then(res3 =>{
        console.log(res3);
    })
}
//方式三 生成器函数实现
function* getData(){
    const res1 = yield requestData("aaa");
    console.log("res1",res1);

    const res2 = yield requestData(res1+"bbb");
    console.log("res2",res2);

    const res3 = yield requestData(res2+"ccc");
    console.log("res1",res3);
}

const generator =  getData();
generator.next().value.then(res1 =>{
    generator.next(res1).value.then(res2 =>{
        generator.next(res2).value.then(res3 =>{
            generator.next(res3)
        })
    })
})
//方式四 使用async 和 await语法糖(ES8)
async function getData(){
    const res1 = await requestData("aaa");
    console.log("res1",res1);

    const res2 = await requestData(res1+"bbb");
    console.log("res2",res2);

    const res3 = await requestData(res2+"ccc");
    console.log("res1",res3);
}
getData();

19、async 和 await

1、使用async关键字修饰的函数即异步函数,异步函数的返回值为promise对象,异步函数中的错误会被catch捕获

async function foo (){
    //第一种情况:普通返回值,会被当做resolve参数回调
    //return "ning"
    //第一种情况:那么foo().then()会根据new Promise的返回情况执行
    //return new Promise(res =>{})
    //第一种情况:返回了一个thenable对象,那么foo().then()会根据thenable对象中then函数的返回情况执行
    return {
        then:function (resolve,reject){
        }
    }
}
foo().then(res =>{
    console.log("res",res)
})

2、await

  • 在async函数中使用

  • 通常await后跟表达式,该表达式的返回值为promise对象

  • await会等到promise状态变为fufilled时,继续执行下面代码

20、进程和线程

1、描述

  • 进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程。
  • 线程:进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程。、

2、JavaScript线程

浏览器是多进程的,每一个tab页都是一个单独的进程,如果采用共用进程只要某一个卡死就会影响其他。每一个进程中有多个线程,其中一个就是JavaScript线程。JavaScript代码是在单个线程中执行的。 单线程同一时间只能做某一操作,那就意味着某些耗时的操作必然会阻塞代码的执行,比如网络请求、定时器等。

3、事件循环

如果在执行代码时遇到阻塞操作,比如网络请求、定时器、DOM监听等,就将这些操作放入一个事件队列中,等到其他无阻塞操作的代码执行完成后,按照队列先进先出的方式执行事件队列中的操作,将这个过程叫做事件循环。

4、宏任务和微任务

  • 宏任务:ajax、setTimeout、setInterval、DOM监听、UI Render等
  • 微任务:promise的then回调、Mutation Obsever API、queueMicrotask()等

注意:主代码先执行,在执行任何一个宏任务之前,先检查微任务队列是否有任务需要执行

21、代码执行顺序面试题

1、面试题一

setTimeout(function () {
    console.log("setTimeout1");
    new Promise(function (resolve) {
        resolve();
    }).then(function () {
        new Promise(function (resolve) {
            resolve();
        }).then(function () {
            console.log("then4")
        });
        console.log("then2");
    });
});

new Promise(function (resolve){
    console.log("promise1");
    resolve();
}).then(function (){
    console.log("then1");
});

setTimeout(function () {
    console.log("setTimeout2");
});

console.log(2);

queueMicrotask(() => {
    console.log("ququeMicrotask1");
});

new Promise(function (resolve){
    resolve();
}).then(function () {
    console.log("then3");
});


//执行顺序
// promise1
// 2
// then1
// queueMicrotask1
// then3
// setTimeout1
// then2
// then4
// setTimeout2

2、面试题二

async function async1 (){
    console.log('async1 start');
    await async2();
    console.log('async1 end');//会进入微任务队列
}

async function async2 (){
    console.log('async2');
}

console.log("script start");
setTimeout(function () {
    console.log("setTimeout")
},0);

async1();

new Promise (function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});

console.log("script end");

//执行顺序
//script start
//async1 start
//async2
//promise1
//script end
//async1 end
//promise2
//setTimeout

22、防抖函数、节流函数以及深拷贝函数

1、防抖函数的基本实现

// 防抖函数的基本实现,防抖函数的基本原理即触发事件时,不会立即响应,会做相应的延迟响应,如果频繁触发事件,那么响应也会根据触发事件进行顺延.
function jydebounce (fn,delay){
    let timer = null;
    const _debounce = function (){
        timer = setTimeout(() => {
            fn();
            time = null;
        },delay); 
    }
    return _debounce;
}

2、节流函数的基本实现

// 节流函数的基本实现,节流函数的基本原理即频繁的触发事件,但是不会进行实时响应,而是周期性的触发响应
const jythrottle = function (fn,interval){
    let startTime = 0;
    const _throttle = function (){
        let nowTime = new Date().getTime();
        let waiteTime = interval - (nowTime - startTime);
        if(waiteTime <= 0){
            fn()
            startTime = nowTime;
        }
    }
    return _throttle;
}

3、深拷贝函数的基本实现

function isObject (value){
    const valueType = typeof value;
    return (value !== null) && (valueType === "object" || valueType === "function")
}

function deepCope (originValue){
    // 判断参数是否为对象,不是则直接返回
    if(!isObject(originValue)){
        return originValue;
    }
    const newObj = {};
    for (const key in originValue) {
        // 递归实现深度拷贝
        newObj[key] = deepCope(originValue[key]);
    }
    return newObj;
}