你不知道的JavaScript之路

前言

想要成爲一個專業的前端er,學習JavaScript是一條必經之路。曾經的我是一個前端新手時,只會寫點html+css,可是不敢寫JavaScript,以爲這個太難了,看到它就懼怕,可是後來仍是硬着寫,寫了一段時間之後感受JavaScript寫着還行,逐漸地也就喜歡上這門語言了。接下來我根據我本身學過的JavaScript技術寫點學習總結,大致內容是函數做用域,閉包,this指向,原型鏈,ES6經常使用問題等等。

1.函數做用域

1.1 做用域鏈查找機制

<script>
//全局做用域

function add(a){
    //add函數做用域
    console.log(a + b);
}

var b = 2;
add(1);
</script>複製代碼

上述代碼執行 add()後會再執行console.log()這句代碼,但括號裏有a和b兩個變量相加,js引擎就會先在add()函數做用域內尋找a和b兩個變量,此時a做爲函數的實參被找到並被賦值到了括號裏的a變量,但此時還未找到b變量的值,這個時候引擎就會往外層嵌套的做用域裏去尋找b這個變量,直到引擎在最外層做用域(全局做用域)還未找到b變量時就會拋出Uncaught ReferenceError: b is not defined(引用錯誤,b變量未定義)javascript

1.2 變量提高

//通常咱們定義變量是先聲明再賦值使用的
    var a;
    a = 1;
    console.log(a);  //此時會打印出a的值爲1
//但還有另外一種寫法,獲得的結果也相同
    a = 1;
    console.log(a);  //這時也會打印出1
    var a;複製代碼

另外一種寫法就是變量提高的一個案例體現,因爲JavaScript是沒有編譯階段的,它是邊解釋邊執行的,因此它會有一個預解釋的過程,函數聲明和變量聲明每次會被解釋器提到方法體的最頂層,這也是聲明提高的概念。接下來再看另外一個案例:css

//初始化a和b
var a = 1;
var b = 2;
console.log(a,b); //這裏會打印出a和b的值也就是1,2

//再看看另外一個寫法
var a = 1;
console.log(a,b); //這裏會打印出1,undefined
var b = 2;複製代碼

產生上面代碼兩種結果的緣由實際上是由於 var b被提高了,可是初始化的var b=2並無被提高,這說明在js裏只有聲明的變量纔會被提高,初始化的不會。變量提高後的代碼以下:html

//因爲b的值初始化時undefined, js也是按照上下文執行的,因此此時打印b結果纔是undefined
var a = 1;
var b;
console.log(a,b);
b = 2;複製代碼

1.3 函數提高

add();

function add(){
    console.log(1);
}複製代碼

函數提高與變量提高是同樣的,定義完add函數之後,它會被提高到最頂層,而後add就能夠調用到了, 但有的寫法不行。前端

add();

var add = function(){
    console.log(1);
}複製代碼

此時控制檯會打印出Uncaught TypeError: add is not a function(類型錯誤:add不是一個函數),由於這個時候觸發的是變量提高,var add被提高到了最頂層,它的初始化值也就是undefined,因此纔會報錯。java

1.4 聲明提高綜合應用總結

var a;
function a(){}
console.log(a); //打印出function a()複製代碼

聲明提高的順序是變量聲明優先於函數聲明,可是函數的聲明會覆蓋未定義的同名變量,再看另外一個例子:es6

//例子
var a = 1;
function a(){}
console.log(a); //打印出1

//例子等價於下面代碼
var a;
function a(){}
a = 1;
console.log(a);複製代碼
  • 重複的變量聲明是無效的,由於它會被提多個var a上去,可是不管前面是什麼,後面的函數聲明都能將其覆蓋。
  • 因爲聲明提高的順序問題,同名的函數聲明會優先於變量聲明。
  • 後面的函數聲明會覆蓋掉前面的函數聲明。


再看看一個筆試題數組

console.log(a);
var a = 1;
function foo(){
    console.log(a);
    var a = 2;
    console.log(a);
}
foo();
console.log(a);
//打印順序結果是 undefined undefined 2 1 複製代碼

把上面的代碼經過變量提高之後:瀏覽器

var a;  
console.log(a);  //第一處打印
a = 1;
function foo(){
    var a;
    console.log(a);  //第二處打印
    a = 2;
    console.log(a);  //第三處打印
}
foo();
console.log(a);   //第四處打印複製代碼

  • 第一個打印a的值是全局做用域的初始化值a也就是undefined,。
  •  第二個打印a的值是函數做用域的初始化a也是undefined。
  • 第三個打印a的值是已經在函數做用域被賦值2的值也就是2。
  • 第四個打印a的值是在全局做用域被賦值1的值也就是1。

1.5 執行流

js由全局環境開始執行,以下圖當全局環境執行到fn1(50)時,此時就會去執行fn1的環境,而後fn1執行到fn2(20)和fn(30)時就會再去執行fn2的環境,直到fn2()被執行完成。緩存



1.6 函數上下文環境

var a = 1;
var b = 2;

function fn1(c){
    var a = 10;
    function fn2(c){
        var a = 100;
        b = a + c;
        console.log(b);
    }
    fn2(20);
    fn2(30);
}
fn1(50);複製代碼

每一個執行環境中都有一個對應的變量對象,它把環境中定義的變量和函數都保存在這個對象裏。bash




2.閉包


2.1 閉包的概念

紅寶書 上對於閉包的定義:閉包是指有權訪問另一個函數做用域中的變量的函數
MDN 對閉包的定義爲:閉包是指那些可以訪問自由變量的函數。 (其中自由變量,指在函數中使用的,但既不是函數參數arguments也不是函數的局部變量的變量,其實就是另一個函數做用域中的變量。)

var a = 1;
function fn1(){
    var b = 2;
    function fn2(){
        console.log(b);
    }
    return fn2;
}
var fn = fn1();
fn();
// console.log(b) 這裏會打印出 b is not defined複製代碼

先來理一下上面代碼的做用域,上面函數分爲全局做用域、fn1做用域、fn2做用域。在全局做用域下訪問b變量,因b變量是在f1做用域內部定義的,js的查找機制也是從裏到外的,故b變量沒法找到,因此纔會打印出b變量未定義。可是在fn2做用域下訪問b變量,fn2就會先在fn2做用域下查找b變量,若是未找到就會一直往外層做用域查找有沒有b變量也就是會在最近的fn1做用域下找到b變量而後直接賦給fn2的b。

這樣看,閉包就是fn2能訪問到其餘外層做用域的變量,可是外層做用域不能直接訪問到內部做用域的變量,也能夠理解爲定義在一個函數內部的函數,閉包的本質是函數內部和函數外部之間鏈接的一條橋樑。

2.2 閉包的應用

//閉包用做計數器
//被用做讀取函數內部的變量,這些變量始終被存在內存裏
function sum(){
    var n = 0;
    function inc(){
        return n++;
    }
    return inc;
}
var inc2 = sum();
console.log(inc2()); //打印出 0
console.log(inc2()); //打印出 1
console.log(inc2()); //打印出 2
inc2 = null; //釋放該內存複製代碼

計數器的閉包函數被建立之後,將sum返回的inc函數給了inc2,而後返回的n變量存在inc2的內存塊內,三次打印inc2()的值就是調用了三次 n++ ,故三次打印依次打印出了0、一、2,最後再將inc2的內存塊釋放掉,由於閉包使得函數裏的變量始終存在內存裏,內存會佔不少消耗,最終會形成瀏覽器性能問題也就是內存溢出問題。

//建立私有變量和私有函數
function student(name){
    var age;
    function setAge(a){
        age = a;
    }
    function getAge(){
        return age;    
    }
    return{
        name:name,
        setAge: setAge,
        getAge: getAge
    }
}

var s = student('cc'); // name:'cc'
s.setAge(20);  // age: 20
console.log(s.name,s.getAge()); //打印出 cc, 20
s = null;複製代碼

在student函數裏建立了一個私有變量age,使用私有函數setAge去間接給age賦值,使用私有函數getAge返回age的值。

2.3 閉包的注意事項

function sum(){
    var arr = [];
    for(var i = 0; i< 10; i++){
        arr[i] = function(){
                return i;
        }
    }
    return arr;
}
var sum2 = sum();
console.log(sum2[0]()); //打印出 10複製代碼

這個時候就會疑惑爲何打印不是0,實際上是由於sum[0]返回的是一個函數,根據查找機制函數返回的i返回的是循環結束之後i++的值也就是10,那麼把代碼修改一下改爲想要的結果:

//es6 將var改爲let
function sum(){
    var arr = [];
    for(let i = 0; i< 10; i++){
        arr[i] = function(){
                return i;
        }
    }
    return arr;
}
var sum2 = sum();
console.log(sum2[0]()); //打印出 0

//自執行函數
function sum(){
    var arr = [];
    for(let i = 0; i< 10; i++){
        arr[i] = (function(n){
                return function(){
                       return n;
                }
        })(i)
    }
    return arr;
}
var sum2 = sum();
console.log(sum2[0]()); //打印出 0複製代碼

第一個例子裏ES6的let與var區別後面再說,再看第二個例子自執行函數將每次循環的i值當作實參賦給了形參n,而後再將n返回出去,最後就獲得了每次循環的i值。

function sum(){
    var n = 0;
    function inc(){
        return n++;
    }
    return inc;
}
//這裏我將sum()實例化後的inc2註釋掉,用用自執行函數
//var inc2 = sum();
console.log(sum()()); //打印0
console.log(sum()()); //打印0
console.log(sum()()); //打印0複製代碼

這裏使用了三次自執行函數,打印仍是0的緣由實際上是每次調用sum()()都是獨自生成了一個內存塊,調用了三次也就是生成了三個不一樣的內存塊存儲n值。

2.4 模擬緩存機制

//函數求和,可是每次執行完之後保存每次放進入的數字
//至關於於Object的key和value
//例如:var abc = { "1,2,3" : 6 }

var save = function(){
    var obj={}
    function fn(){
        var sum = 0;
        for(var i =0;i<arguments.length;i++){
             sum = sum + arguments[i];
        }
        return sum; 
    }
    return function(){
        //將arguments強轉換成數組而後執行Array.join()方法
        var arg = Array.prototype.join.call(arguments, ',');
        obj[arg] = fn.apply(null, arguments);
        console.log(obj);
        for(var i = 0;i<Object.keys(obj).length;i++){
            console.log(Object.keys(obj)[i].split(',').map(Number));
        }
    }
}();

save(1,2,3,4,5);
save(1,2,3,4,5,6,7);
save();複製代碼

運行結果


能夠看到每次調用save()函數之後,每次存進去的參數都會被保存在obj集合裏,obj集合的key就是每次保存進去的全部參數,obj集合的value值就是每次傳入參數後計算後的總和值。

3.this指向

3.1 非嚴格模式和嚴格模式下的this指向

this === window;  //true
 'use strict';
this === window; //true
this.n = 10;
console.log(this.n); //打印出10複製代碼

非嚴格模式和嚴格模式下this都指向的是最頂層做用域(瀏覽器是window)

//非嚴格模式下
var n = 10;
function fn(){
    console.log(this);  //打印出window
    console.log(this.n); //打印出10
}
fn();

//嚴格模式下
'use strict';
var n = 10;
function fn(){
    console.log(this); //打印出undefined
    console.log(this.n); //報錯TypeError
}複製代碼

非嚴格模式下函數裏的this指向window ,this.n則能夠打印出10。但嚴格模式下this則指向undefined,故打印this.n時瀏覽器會打印出TypeError:Cannot read property 'n' of undefined(沒法讀取到未定義的屬性n)

3.2 隱式綁定

var n = 1;
function fn(){
    console.log(this.n);
}
var obj = {
    n:5,
    fn:fn,
    obj2:{
        n:10,
        fn:fn
    }
}
obj.fn();  //打印5
obj.obj2.fn(); //打印10複製代碼

  • 第一次調用fn()函數的直接對象是obj,此時this指向了obj
  • 第二次調用fn()函數的直接對象是obj2,此時this指向了obj2

3.3 隱式綁定丟失this指向

var n = 1;
function fn(){
    console.log(this.n);
}
var obj = {
    n:5,
    fn:fn
}

var fn2 = obj.fn;
fn2();  //此時打印的是1 而不是5複製代碼

將obj.fn賦值給了fn2,因爲fn2是在window指向下的,故fn2()去調用fn()函數時this指向從obj內部指向了window。

var n = 1;
function fn(){
    console.log(this.n);
}
var obj = {
    n:5,
    fn:fn
}

setTimeout(obj.fn,1000) //一秒後打印出1複製代碼

內置函數setTimeout和setInterval這種,第一個參數回調函數裏的this默認指向的是window。

var n = 1;

var obj = {
    n:5,
    fn:()=>{
        console.log(this.n);
    }
}
obj.fn(); //打印出1複製代碼

ES6的箭頭函數與普通函數不一樣,箭頭函數中沒有this指向,它必須經過查找做用域鏈來決定this的值,若是箭頭函數包含在一個普通函數裏,則它的this值會是最近的一個普通函數的this值,不然this的值會被設置成全局變量也就是window。

3.4 顯式綁定

var n = 1;
function fn(){
    console.log(this.n);
}
var obj = {
    n:10
}

fn(); //打印出1
fn.call(obj); //打印出10
fn.apply(obj); //10
fn.bind(obj)(); //10複製代碼

call、apply、bind方法的第一個參數是this指向的目標,它會強制改變this的指向,而且使得this不能再被改變。

3.5 new綁定

var n = 1;

function Fn(n){
    this.n = n;
    console.log(this);  //打印出 Fn: { n:10 }
    //return {}
    //return function f(){}
}

var fn = new Fn(10);複製代碼

new操做符調用時this會指向生成的新對象,可是new調用的返回值沒有顯示返回對象或者函數,纔是返回生成的新對象。

4.原型鏈

4.1 構造函數、實例對象、原型對象的關係

function Foo(name){
    this.name = name;
}
var foo = new Foo('CC');

console.log(foo.constructor === Foo); //true
console.log(foo.__proto__ === Foo.prototype); //true
console.log(Foo.prototype.constructor === Foo); //true複製代碼


Foo()函數被new實例後成爲實例對象foo之後,foo實例對象裏產生了構造函數constructor和__proto__原型屬性,foo的constructor指向Foo自己,控制檯打印foo.constructor會把Foo這個函數自身顯示出來。而foo的原型屬性_proto__則指向了Foo的原型對象屬性prototype,固然Foo的原型對象屬性prototype的構造函數constructor是指向了Foo自身。

__proto__和prototype看起來很類似,可是二者仍是有點區別的,__proto__存在於全部的對象上,prototype存在於全部的函數上,從上面例子能夠看到foo是一個實例對象因此它只擁有__proto__屬性,但沒有prototype屬性,嘗試去打印foo.prototype能夠看到結果是undefined,可是在js裏函數也是對象的一種,因此在Foo裏__proto__屬性和prototype屬性都會同時擁有。

4.2 對象的原型鏈


JavaScript經過 __proto__屬性指向父類對象,直到指向Object對象爲止,這樣造成了一個原型的鏈條就叫作原型鏈,原型鏈的盡頭也就是Object.prototype,由於再往下指就是null了。

4.3 模擬實現ES6的class

//ES6 class語法
class Square{
    constructor(edge){
        this.edge = edge;
    }
    
    getEdge(){
        console.log(`正方形的邊長是${this.edge}`);
    }
}
new Square(5).getEdge();  //打印出 正方形的邊長是5

//用原型鏈模擬實現class語法
var Square = (function (){
    function Square(edge){
        this.edge = edge;
    }
    
    Square.prototype.getEdge = function(){
            console.log(`正方向的邊長是${this.edge}`);
        }
    return Square;
})();
new Square(3).getEdge(); //打印出 正方形的邊長是3複製代碼

由上面能夠看出ES6的class其實就是構造器的語法糖在class裏定義的函數其實就是放在了構造器的prototype裏。

4.4 模擬實現ES6的extends

//ES6的class繼承
class Person{
    constructor(name){
        this.name = name;
    }
}

class Student extends Person{
    constructor(name, number){
        super(name);
        this.number = number;
    }
    getView(){
        console.log(`學生姓名是${this.name},學號是${this.number}`);
    }
}
new Student('小米',20200101).getView();  //打印出 學生姓名是小米,學號是20200101

//使用原型繼承和組合繼承模擬ES6的繼承
//原型繼承
function inheritsLoose(child,parent){
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
    child.__proto__ = parent;
}

var Person = function(name){
    this.name = name;
}

var Student = (function(_Person){
    inheritsLoose(Student,_Person);
    
    function Student(name,number){
        var _this;
        //組合繼承
        _this = _Person.call(this,name) || this;
        _this.number = number;
        return _this;
    }
    
    Student.prototype.getView = function(){
        console.log(`學生姓名是${this.name}, 學號是${this.number}`);
    }
    return Student;
})(Person);

new Student('小米',20200101).getView(); //打印出 學生姓名是小米,學號是20200101複製代碼

ES6的繼承機制其實就是實現了原型繼承和組合繼承,子類構造器調用父類構造器並將this指向了子類構造器。

5.ES6經常使用問題

5.1 var與let、const的區別

ES6裏引入了一個塊級做用域,它存在於函數內部或者{ }中,接着就有了塊級聲明,塊級聲明用來聲明在指定塊的做用域外沒法訪問的變量。

ES6裏用let和const來當作塊級聲明去聲明變量,爲的就是控制變量的生命週期,用var聲明時會出現不少問題,好比變量提高,能重複聲明變量等,可是用let和const時就不會再產生該問題了。

//不存在變量提高
let a;
console.log(a);  //打印出 undefined
a = 1;

//不能重複聲明
let b;
let b = 2;
console.log(b); //打印出SyntaxError錯誤,b變量已經被聲明瞭

//不存在污染變量
for(let i = 0;i<3;i++){
   //dosth
}
console.log(i); //打印出ReferenceError錯誤, i變量未定義

//不綁定全局做用域
let c = 1;
function fn(){
    console.log(this.c); //打印出 undefined
}
fn()複製代碼

接下來再來看看let和const的區別,const用於定義常量,定義結束之後不容許被修改,不然會報TypeError的錯誤,雖然const定義後不能被修改其值,但容許被修改內部的值,例如當用const定義一個object類型時:

const obj = { a:1 };
obj.a = 2;
obj.b = 3;
console.log(obj); //打印出 { a:2, c:3 }複製代碼

5.2 call、apply、bind的區別

var m = 1, n = 2;
function fn(){
    console.log(this.m, this.n);
}
var obj = {
    m : 5,
    n : 10
}
fn(); //打印 1,2
fn.call(obj, m, n); //打印 5,10
fn.apply(obj,[m,n]); //打印 5,10
fn.bind(obj,m,n)(); //打印 5,10複製代碼

從上面例子裏很容易就能夠看到三者的共同點都能改變this指向,接下來再說三者的區別:

  • call第一個參數是用來this指向,後面能夠傳入多個參數
  • apply第一個參數是用來this指向,後面只能把多個參數做爲一個數組傳進去
  • bind第一個參數是用來this指向,後面跟call用法同樣能夠傳入多個參數,可是它最後返回的是一個函數,要使用它的話須要間接調用。

5.3 扁平化數組排序篩選

//將數組扁平化後去重並按數字大小從大到小排序最後再留下小於50的數字
var a = [49,[12,14,25,7],[23,53,25,[98,9,[65,25,20]]],65,20,9];複製代碼

  • 數組扁平化 ( Array.flat( )或者其餘方案 )
  • 數組去重     ( new Set( ) )
  • 數組排序     (Array.sort( ))
  • 數組篩選     ( Array.filter( ) )

//用flat()實現扁平化 上面最深的嵌套有3層

let b = Array.from(new Set(a.flat(3))).sort((a,b)=> b - a).filter(i => i < 50);

//用ES6的generator函數實現扁平化
function* flatUp(array){
    for(let item of array){
        if( Array.isArray(item) ){
            yield* flatUp(item);
        }else{
            yield item;
        }
    }
}

let b = Array.from(new Set([...flatUp(a)])).sort((a,b)=> b - a).filter(i => i < 50);複製代碼

5.4 深淺拷貝

首先得理解堆內存和棧內存的區別:

基本數據類型(如number,String類型)都會直接存儲在棧內存裏,但引用數據類型(如Object,Array類型)在棧內存中存儲的是指針位置,實際真實數據存儲在堆內存裏,該指針指向堆存儲的該實體的起始地址。


5.4.1 深淺拷貝與直接賦值的區別

深拷貝和淺拷貝都是針對引用數據類型(Object,Array)的方案


深淺拷貝的區別:淺拷貝是複製指向對象的指針,而不復制整個對象的自己,新舊對象使用的是同一個內存塊。可是深拷貝會建立一個與原來如出一轍的對象,而且不共用同一個內存塊,修改新對象時不會改到原對象。

先來看看淺拷貝和普通直接賦值的區別:

//直接賦值
var obj1 = {
    n: 2,
    arr:[1,[2,3]]
}

var obj2 = obj1;
obj2.n = 1;
obj2.arr[1] = [5,6,7];
console.log(obj1); //obj1: { n:1, arr:[1,[5,6,7]] }
console.log(obj2); //obj2: { n:1, arr:[1,[5,6,7]] }

//淺拷貝
function shallowCopy(obj){
    var data = {}
    for(let item in obj){
        if(obj.hasOwnProperty(item)){
            data[item] = obj[item];
        }
    }
    return data;
}

var obj1 = {
    n: 2,
    arr:[1,[2,3]]
}

var obj3 = shallowCopy(obj1);
obj3.n = 1;
obj3.arr[1] = [5,6,7];
console.log(obj1); //obj1: { n:2, arr:[1,[5,6,7]] }
console.log(obj3); //obj3: { n:1, arr:[1,[5,6,7]] }複製代碼
  • 直接賦值是將obj1直接賦值給obj2過程當中是至關於將obj1的內存地址賦給了obj2,而不是堆中的數據,因此obj2和obj1二者是聯動的,當obj2改變屬性之後obj1也會被改變。
  • 淺拷貝是將obj1的屬性依次拷貝後並建立一個新對象也就是obj3,若是拷貝的屬性是基本數據類型,拷貝的就是基本數據類型的數值,但若是拷貝的屬性是引用數據類型,拷貝的就是引用數據類型的地址,因此當obj3改變的屬性是引用數據類型時也會影響到obj1的引用數據類型的屬性值,可是不會影響基本數據類型的屬性值。
  • 深拷貝顧名思義就是會建立一個徹底的新的對象,不管更改的是基本數據類型的屬性仍是引用數據類型的屬性都徹底不會影響到原來的對象。

5.4.2 淺拷貝的三種實現方式(Object.assign( )、Array.slice( )、Array.concat( ))

1. Object.assign()

//多重嵌套時Object.assign()是淺拷貝
var obj = { a:{a:1,b:2}};
var obj2 = Object.assign({},obj);
obj2.a.a = 10;
obj2.a.c = 5;
console.log(obj); //{a:{a:10,b:2,c:10}}
console.log(obj2); //{a:{a:10,b:2,c:10}}


//當Object只有一層時Object.assign()是深拷貝
var obj = { a:1 };
var obj2 = Object.assign({},obj);
obj2.a = 10;
console.log(obj); //{a:1}
console.log(obj2); //{a:10}複製代碼

2. Array.slice()

var arr = [1,2,{a:1}];
var arr2 = arr.slice();
arr2[1] = 5;
arr2[2].a = 5;
console.log(arr); // [1,2,{a:5}]
console.log(arr2); // [1,5,{a:5}]複製代碼

3. Array.concat()

var arr = [1,2,{a:1}];
var arr2 = arr.concat();
arr2[1] = 10;
arr2[2].a = 10;
console.log(arr); // [1,2,{a:10}]
console.log(arr2); // [1,10,{a:10}]複製代碼

5.4.3 深拷貝的三種實現方式(JSON.parse(JSON.stringify( ))、遞歸、lodash庫)

1. JSON.parse(JSON.stringify( ))

//這種方法只能用來深拷貝數組或者對象,不能用於拷貝函數
var arr = [1,2,{a:1}];
var arr2 = JSON.parse(JSON.stringify(arr));
arr2[1]=10;
arr2[2].a=10;
console.log(arr); // [1,2,{a:1}]
console.log(arr2); // [1,10,{a:10}]複製代碼

2. 遞歸

function deepClone(obj){
    let result = typeof obj === 'function' ? [] : {};
    if(obj && typeof obj === 'object'){
        for(let i in obj){
            if(obj[i] && typeof obj[i] === 'object'){
                result[i] = deepClone(obj[i]);
            }else{
                result[i] = obj[i];
            }
        }
        return result;
    }
    return obj;
}複製代碼

3. lodash庫

//lodash函數庫使用 _.cloneDeep()
const _ = require('lodash');
var obj = {
    a:1,
    b:{c:2}
}
var obj2 = _.cloneDeep(obj);複製代碼


若有錯誤或者缺漏,歡迎指點。

相關文章
相關標籤/搜索