[Dịch] Cùng khám phá về deep copy trong javascript

Đăng lúc 29/10/2018

Hôm trước khi làm việc với Vue mình gặp phải một vấn đề với JavaScript về việc sao chép các đối tượng tham chiếu. Trong lúc lang thang trên segmentfault mình vô tình tìm được một bài viết khá hay và chi tiết về vấn đề này. Nên mình quyết định dịch ra tiếng Việt để chia sẽ cho các bạn.

Nói một tí về vị tiền bối này. Anh ta có tên tiếng Trung là 颜海镜 (Nhan Hải Kính). Xin Nhan tiên sinh thứ lỗi nếu mình lỡ dịch sai tên sang Hán Việt. Là một lập trình viên thuộc thế hệ 9x (90后). Hoạt động trong nhiều lĩnh vực kỹ thuật khác nhau. Các bạn có thể theo dõi anh ta ở địa chỉ yanhaijing.com.

Hiện tại Nhan tiên sinh đang làm việc tại một công ty cũng khá nổi tiếng bên Trung Quốc là 美团点评 - Meituan Dianping với công việc là lập trình viên frontend. Một trong các dịch vụ của công ty là giao thức ăn. Các bạn có thể xem clip quảng cáo này của họ. Khá hay đấy :))

Link bài viết gốc: 深拷贝的终极探索(99%的人都不知道)

Tập trung vào trọng điểm thì đây là một câu hỏi phỏng vấn như bao câu hỏi phỏng vấn thôi. Những đã có kha khá ứng viên bị tôi loại bởi vấn đề này đấy.

Thứ nhất, đây là một câu hỏi phỏng vấn khá hay. Các bạn có thể xem xét khá nhiều khía cạnh của ứng viên như kỹ năng cơ bản, khả năng code, logic cùng sự linh hoạt. Tùy theo từng cấp độ của ứng viên mà chúng ta có các xem xét đánh giá khác nhau. Ví dụ như một cô nàng xinh đẹp thì ngại gì cho câu hỏi ở độ khó 1 sao nhỉ. Còn với các đấng nam tử thì cũng không ngại đối đầu với những câu hỏi 5 sao đâu nhỉ (*^__^*) 嘻嘻...

Trong bài viết hôm nay, tôi sẽ cung cấp cho các bạn bốn phương thức deep copy, với mỗi cách sẽ có đặc điểm và tính chất riêng.

Deep copy VS shallow copy

Trước khi bắt đầu, bạn cần phải biết một chút về deep copy. Nếu bạn đã biết rồi thì bạn có thể bỏ qua bước này

Thực tế thì Deep copy và shallow copy đều là các kiểu tham chiếu. Các kiểu biến trong JS được chia ra làm các loại là kiểu tham chiếu và kiểu giá trị. Đối với kiểu giá trị thì quá trình copy sẽ là việc copy giá trị sang biến mới. Đối với kiểu tham chiếu thì quá trình copy sẽ là copy địa chỉ qua biến mới, nghĩa là cả hai biến sẽ chỉ vào cùng một giá trị.


// copy giá trị cơ bản
var a = 1;
var b = a;
a = 2;
console.log(a, b); // 2, 1 ,a và b có giá trị khác nhau

// copy kiểu tham chiếu với cùng một dữ liệu
var a = {c: 1};
var b = a;
a.c = 2;
console.log(a.c, b.c); // 2, 2 => tất cả đều là 2,a và b có giá trị như nhau

Vì là kiểu tham chiếu, nên a và b sẽ được dẫn đến cùng một dữ liệu. Nếu bạn thay đổi một trong số chúng, những thay đổi đó sẽ tác động đến tất cả các biến cùng tham chiếu đến. Đôi khi điều này sẽ gây ra những kết quả không muốn. Nên nếu bạn không thông suốt những khái niệm này thì nó có thể gây ra những bug không cần thiết.

Nhằm phá vỡ mối quan hệ của a và b. Chúng ta có thể copy dữ liệu theo các mức độ từ shallow đến deep. Shallow copy thì chỉ có thể copy được một lớp còn với deep copy thì các bạn có thể copy sâu đến tận cùng đối tượng.


var a1 = {b: {c: {}};

var a2 = shallowClone(a1); // shallow copy
a2.b.c === a1.b.c // true

var a3 = clone(a3); // deep copy
a3.b.c === a1.b.c // false

Việc thực hiện shallow copy khá đơn giản và nhiều cách. Vì thực tế việc cần làm chỉ là duyệt qua các thuộc tính của đối tượng như dưới đây là một cách. Nếu bạn không hiểu phần nào trong đoạn code dưới đây hoặc bạn muốn tìm hiểu thêm nhiều cách khác bạn có thể tham khảo bài viết này (tiếng Trung)


function shallowClone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            target[i] = source[i];
        }
    }

    return target;
}

Cách thực hiện deep copy đơn giản nhất

Phương pháp deep copy này được xây dựng từ hai điều là shallow copyđệ quy. Giả sử chúng ta có một biến như bên dưới.


var a1 = {b: {c: {d: 1}};

Chúng ta sẽ cập nhật lại hàm một tí từ hàm của shallow copy bên trên. Các bạn hãy chú ý đến điểm khác biệt nhé.


function clone(source) {
    var target = {};
    for(var i in source) {
        if (source.hasOwnProperty(i)) {
            if (typeof source[i] === 'object') {
                target[i] = clone(source[i]); // cần lưu ý một tí ở đây
            } else {
                target[i] = source[i];
            }
        }
    }

    return target;
}

Hầu hết mọi người đều có thể viết được đoạn code trên. Nhưng khi tôi hỏi về những vấn đề nằm trong đoạn code trên thì không nhiều người có thể trả lời được. Còn bạn, bạn có thấy được những vấn đề trong đó không?

Thực tế thì đoạn code trên có quá nhiều vấn đề. Tôi sẽ đưa ra một vài ví dụ:

  • Không hề kiểm tra các giá trị đầu vào
  • Xác định logic của đối tượng không đủ nghiêm ngặt
  • Không hề xem xét khả năng tương thích đối với mảng

Chúng ta hãy cùng nhìn nhận các vấn đề trên và tìm các giải pháp tương ứng cho chúng. Trước tiên, chúng ta cần tìm cách để xác định giá trị đầu vào là dạng đối tượng. Thực tế cũng chỉ ra rằng có khá nhiều cách thông thường để làm việc này ví dụ như cách dưới đây. Và thực tế cũng cho thấy cách này cũng có một số vấn đề, sẽ thật tốt nếu như bạn tìm được những vấn đề đó. Còn nếu bạn quan tâm đến các hoàn hoản nhất trong trước hợp này bạn có thể xem ở đây


function isObject(x) {
    return Object.prototype.toString.call(x) === '[object Object]';
}

Hàm deep copy sẽ bổ sung đoạn kiểm tra các tham số đầu vào. Nếu nó không phải là object thì sẽ được trả về ngay lập tức.


function clone(source) {
    if (!isObject(source)) return source;

    // xử lý
}

Còn về vấn đề thứ ba thì tôi sẽ để các bạn tự suy nghĩ về điều đó. Đồng thời để giảm bớt gánh nặng cho việt đọc bàn viết này nên tôi sẽ không xem xét vấn đề này. Hiện nay, sau ES6 các bạn có thể xem xét một số phương thứ như sau set, map, weakset, weakmap

Thật ra thì đó cũng chỉ là ba vấn đề nhỏ thôi. Vấn đề lớn nhất cần quan tâm đó chính là việc sử dụng đệ quy có thể gây ra stack overflow. Khi mà dữ liệu chúng ta cần sao chép có độ sâu cao và kích thước lớn nó có thể gây ra các sự cố về bộ nhớ và ảnh hưởng xấu đến hiệu năng.

Bây giờ, các bạn hãy tập trung vào đoạn code dưới đây. Đây là đoạn code với chức năng tạo ra đối tượng với những quy định về độ rộng và sâu của đối tượng. Đoạn code này sẽ được dùng lại khá nhiều trong bài viết hôm nay đấy.


function createData(deep, breadth) {
    var data = {};
    var temp = data;

    for (var i = 0; i < deep; i++) {
        temp = temp['data'] = {};
        for (var j = 0; j < breadth; j++) {
            temp[j] = j;
        }
    }

    return data;
}

createData(1, 3); // dữ liệu được tạo ra với độ rộng là 3 và độ sâu là 1 {data: {0: 0, 1: 1, 2: 2}}
createData(3, 0); // dữ liệu được tạo ra với độ rộng là 0 và độ sâu là 3 {data: {data: {data: {}}}}

Khi chúng ta gọi hàm clone với đối tượng có độ sâu lớn thì xảy ra hiện tương stack overflow nhưng với những dữ liệu có độ rộng lớn thì không xảy ra những hiện tượng trên. Các bạn có thể chứng minh với đoạn code bên dưới.


clone(createData(1000)); // ok
clone(createData(10000)); // Maximum call stack size exceeded

clone(createData(10, 100000)); // vẫn ổn với độ rộng lên đến 100000

Trong thực tế thì những dữ liệu có độ sâu như trên (10000) hiếm khi xuất hiện. Nhưng lại có một vấn đề nghiêm trọng xảy ra với cách sao chép này đó là hiện tượng tham chiếu vòng tròn. Ví dụ


var a = {};
a.a = a;

clone(a) // Maximum call stack size exceeded (Đệ quy vô tận nhé),/(ㄒoㄒ)/~~

Có hai hướng để giải quyết vấn đề tham chiếu vòng tròn trong đệ quy. Thứ nhất là phát hiện ra hiện tượng đó (để đối phó). Hai là tìm cách phá vỡ cái vòng tròn tham chiếu đáng sợ đó. Các bạn có thể suy nghĩ về cách đầu tiên. Còn về việc phá vỡ vòng tròn tôi sẽ nhắc đến ngay sau đây.

Thực hiện deep copy chỉ với một dòng code

Một số bạn sinh viên có thể đã từng biết về việc sử dụng JSON để thực hiện deep copy. Chúng ta có thể xem một đoạn code được miêu tả dưới đây


function cloneJSON(source) {
    return JSON.parse(JSON.stringify(source));
}

Thật lòng mà nói trong lần đầu tôi tiếp cận phương pháp này. Tôi đã rất ngưỡng mộ nó. Bởi lẻ nó đã dùng những phương pháp có sẵn một cách rất thông minh để hoàn thành công việc.

Tôi cũng đã thử kiểm tra những vấn đề liên quan đến stack overflow của phương pháp này. Nó cho thấy phương pháp này có vẻ như cũng đã áp dụng đệ quy (chứ không đơn giản như một dòng code phía trên).


function cloneJSON(source) {cloneJSON(createData(10000)); // Maximum call stack size exceeded

Sau khi biết được đệ quy cũng được dùng trong phương pháp này, đồng thời chắc các bạn cũng chưa quên hậu quả của việc tham chiếu vòng tròn. Vậy nên bây giờ chúng ta sẽ kiểm tra nó có gặp vấn đề với đệ quy vô tận không. Đoạn code bên dưới chỉ ra rằng JSON.stringify có thể phát hiện được tham chiếu vòng tròn. Đây cũng chính là phương pháp đầu tiên tôi đã đề cập ở trên trong việc chống tham chiếu vòng tròn (gây ra đệ quy vô tận). Đó chính là phát hiện tham chiếu vòng tròn.


var a = {};
a.a = a;

cloneJSON(a) // Uncaught TypeError: Converting circular structure to JSON

Phá bỏ đệ quy

Theo tôi, có hai phương pháp dùng để hủy dòng đệ quy. Cách đầu tiên đó là loại bỏ đệ quy đuôi nhưng trong trường hợp này điều đó có vẻ không khả thi lắm. Các thứ hai thì đơn giản hơn đó là ngừng sử dụng đệ quy, thay vào đó chúng ta sẽ vòng lặp. Nhưng khi tôi đề xuất dùng vòng lặp thì có khoảng 90% lập trình viên frontend không thể code được theo hướng đó. Điều này thật sự gây bất ngờ cho tôi.

Giả sử chúng ta có dữ liệu được cấu trúc như sau:


var a = {
    a1: 1,
    a2: {
        b1: 1,
        b2: {
            c1: 1
        }
    }
}

Đây có phải một dữ liệu dạng cây không? Câu trả lời được diễn tả rõ ràng dưới đây.


    a
  /   \
 a1   a2        
 |    / \         
 1   b1 b2     
     |   |        
     1  c1
         |
         1

Để duyệt qua một cái cây thì chúng ta đòi hỏi một stack. Điều kiện là khi nào stack đó không rỗng thì là hợp lệ. Stack đó sẽ lưu trữ các node cần được sao chép.

Đầu tiên, chúng ta sẽ đặt các dự liệu ban đầu vào trong stack, key được dùng để lưu trữ những phần tử cha của các phần tử cần sao chép.

Trong vòng lặp đó, chúng ta sẽ duyệt qua các phần tử con của node hiện tại. Nếu nó là một đối tượng chúng ta sẽ bỏ nó vào stack bên trên. Nếu là giá trị thì chúng ta sẽ sao chép trực tiếp.


function cloneLoop(x) {
    const root = {};

    // stack
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // Ở lớp đầu tiên
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // khởi tạo đối tượng sao chép, nếy key bằng undefined thì sẽ sao chép vào phần tử cha, nếu không thì sao chép vào các phần tử con.
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }

        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // Bỏ dữ liệu vào để dành cho chu kì tiếp theo
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

Sau khi chuyển sang phương pháp vòng lặp, chúng ta có thể thấy sẽ không còn vấn đề với việc stack overflow. Nhưng chúng ta vẫn chưa thể giải quyết được vấn đề tham chiếu vòng tròn.

Giải quyết tham chiếu vòng tròn

Liệu có cách nào để giải quyết việc tham chiếu vòng tròn? Đừng lo lắng, chúng ta hãy xem xét một vấn đề khác. Một trong những vấn đề của ba phương pháp trên đó là việc mất tham chiếu. Trong một vài trường hợp thì điều này khó mà chấp nhận được.

Ví dụ nhé, nếu bạn có một đối tượng a có hai key và cùng tham chiếu đến b. Sau khi thực hiện deep copy xong, hai key đó sẽ mất mối quan hệ tham chiếu dẫn đến chúng trở thành hai đối tượng khác nhau.


var b = 1;
var a = {a1: b, a2: b};

a.a1 === a.a2 // true

var c = clone(a);
c.a1 === c.a2 // false

Khi mà chúng ta phát hiện được một đối tượng mới cần sao chép. Trước khi sao chép nó, chúng ta nên kiểm tra xem đối tượng đó đã được sao chép hay chưa. Nếu đã sao chép rồi thì chúng ta không cần sao chép lại lần nửa để đảm bảo giữ được mối quan hệ cũ.

Vậy chúng ta code như thế nào để hiện thực hóa ý tưởng đó? Đừng vội nhìn xuống code phía dưới nhé. Thực tế phần code này khá giống với đoạn dùng vòng lặp ở trên và những chổ khác biệt tôi đã comment lại cho các bạn rồi.

Tôi cũng xin giải thích một tí về mảng uniqueList, đây là một mảng lưu trữ những phần tử đã được sao chép. Cứ mỗi lần duyệt qua mảng thì việc đầu tiên cần làm là kiểm tra xem đối tượng hiện tại có được sao chép hay chưa (nằm trong array uniqueList). Nếu có, chúng ta sẽ bỏ qua bước sao chép.

find là một hàm đảm nhận chức năng duyệt và kiểm tra đối tượng và mảng uniqueList


// function này sẽ giữ lại các mối quan hệ tham chiếu
function cloneForce(x) {
    // =============
    const uniqueList = []; // các thành phần đã được copy
    // =============

    let root = {};

    // khởi tạo stack (như đoạn code ở phần trước)
    const loopList = [
        {
            parent: root,
            key: undefined,
            data: x,
        }
    ];

    while(loopList.length) {
        // khởi tạo ở độ sâu đầu tiên
        const node = loopList.pop();
        const parent = node.parent;
        const key = node.key;
        const data = node.data;

        // khởi tạo đối tượng sao chép, nếy key bằng undefined thì sẽ sao chép vào phần tử cha, nếu không thì sao chép vào các phần tử con.
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = {};
        }
        
        // =============
        // Kiểm tra xem dữ liệu đã tồn tại hay chưa
        let uniqueData = find(uniqueList, data);
        if (uniqueData) {
            parent[key] = uniqueData.target;
            break; // sao chép vào parent và break vòng lặp ở đây
        }

        // Nếu dữ liệu chưa tồn tại
        // lưu lại giữ liệu gốc, bỏ dữ liệu tham chiếu tương ứng vào.
        uniqueList.push({
            source: data,
            target: res,
        });
        // =============
    
        for(let k in data) {
            if (data.hasOwnProperty(k)) {
                if (typeof data[k] === 'object') {
                    // Bỏ dữ liệu vào để dành cho chu kì tiếp theo
                    loopList.push({
                        parent: res,
                        key: k,
                        data: data[k],
                    });
                } else {
                    res[k] = data[k];
                }
            }
        }
    }

    return root;
}

function find(arr, item) {
    for(let i = 0; i < arr.length; i++) {
        if (arr[i].source === item) {
            return arr[i];
        }
    }

    return null;
}

Kiểm tra thử xem nào


var b = 1;
var a = {a1: b, a2: b};

a.a1 === a.a2 // true

var c = cloneForce(a);
c.a1 === c.a2 // true -> Ngạc nhiên chưa ahihi

Tiếp theo tôi sẽ nói về vấn đề việc phá vỡ tham chiếu vòng tròn. À mà chờ tí nào. Có vẻ đoạn code trên phá vỡ được vấn đề đó. Kiểm tra nhanh xem nào.


var a = {};
a.a = a;

cloneForce(a)

// Kết quả hơi bị trất's nhé. LUL

Có vẻ không được hoàn hảo lắm nhỉ? Chúng ta hãy nhìn vào hai vấn đề của cloneForce.

Vấn đề thứ nhất có thể dùng một câu nói của cổ nhân đó là "Thành cũng Tiêu Hà, bại cũng Tiêu Hà". Nếu giữ lại những tham chiếu là điều bạn không muốn thì trong trường hợp này bạn không thể dùng cloneForce

Thứ hai là khi đối tượng chứa dữ liệu tương đối lớn thì cloneForce sẽ không phải là một phương pháp thích hợp.

So sánh hiệu suất

Nếu các nội dung ở phía trên có vẻ khó nuốt thì... hãy cố thêm một tí gì khó nuốt nửa nhé (lỡ rồi tới luôn). Chúng ta hãy so sánh hiệu suất của các phương pháp ở trên.

Trước tiên, hãy thử nghiệm và xem xét hai điều có thể ảnh đến hiệu suất. Một là chiều sâu còn hai là chiều rộng. Chúng ta sẽ sử dụng một biến để tra hiệu suất bằng cách thay đổi giá trị của biến đó.

Phương pháp kiểm tra này cho ra số lần thực hiện deep copy. Nếu số lượng này càng lớn thì đồng nghĩa phương pháp có hiệu suất càng tốt.

Hàm RunTime sau đây sẽ là một hàm cốt lõi trong quá trình kiểm thử. Trong ví dụ sau, chúng ta sẽ thử kiểm tra với một số lần số lần sao chép làm được trong 2 giây với clone(createData(500, 1)


function runTime(fn, time) {
    var stime = Date.now();
    var count = 0;
    while(Date.now() - stime < time) {
        fn();
        count++;
    }

    return count;
}

runTime(function () { clone(createData(500, 1)) }, 2000);

Trong lần thử nghiệm đầu tiên, chúng ta giữ động rộng là 100 và cho thay đổi độ sâu từ nhỏ đến lớn. Sau đó, ghi lại số lần thực hiện trong thời gian là một giây.

Chiều sâu clone cloneJSON cloneLoop cloneForce
500 351 212 338 372
1000 174 104 175 143
1500 116 67 112 82
2000 92 50 88 69

Dựa vào bảng dữ thống kê ở phía trên chúng ta có thể đưa ra một số điều như sau:

  • Ở những độ sâu lơn dân, độ chênh lệch sẽ dần được thu hẹp.
  • Độ chênh lệch giữa clonecloneLoop thì không lớn lắm
  • cloneLoop > cloneForce > cloneJSON

Trước hết, hãy phân tích độ phức tạp của từng phương thức. Có một điều mà các phương thức đều làm mà ở đây chúng ta không tính toán ví dụ như xác định xem kiểu biến có phải là dạng đối tượng không

  • Thời gian clone = thời gian tạo đệ quy + thời gian xử lý cho từng đối tượng
  • Thời gian cloneJSON = thời gian phát hiện loop + thời gian xử lý từng đối tượng * 2 (Đệ quy đưa về chuỗi + đệ quy đưa về object)
  • Thời gian cloneLoop = Thời gian xử lý cho từng đối tượng
  • Thời gian cloneForce = Kiểm tra nhằm bỏ qua đối tượng đã được lưu trữ + thời gian xử lý từng đối tượng

cloneJSON thì chậm hơn clone khoảng 50%. Thật dễ hiều bởi vì nó tốn thêm thời gian đệ quy.

Bởi vì cloneForce kiểm tra xem đối tượng có nằm trong cache hay chưa nên tốc độ của nó cũng khá chậm. Tôi cũng phán đoán được độ phức tạp của nó dựa trên logic sử dụng. Giả sử số đối tượng là n, thì độ phức tạp sẽ là O(n2). Tương đương với việc số lượng càng nhiều thì hiệu năng càng giảm.

1 + 2 + 3 ... + n = n^2/2 - 1

Có một vấn đề với clonecloneLoop đó là kết quả thử nghiệm có vẻ không được khớp với những gì chúng ta đã suy luận. Chắc hẳn chúng ta đã có những lổ hổng trong suy luận

Tiếp theo, chúng ta sẽ lại thử nghiệm với bài kiểm thử thứ hai. Chiều sâu lúc này sẽ là 10000 đồng thời chiều rộng cố định là 0 và chúng ta sẽ đếm số lần thực hiện được trong 2 giây.

Chiều rộng clone cloneJSON cloneLoop cloneForce
0 13400 3272 14292 989

Sau khi loại trừ sự can thiệp của chiều rộng trong các thí nghiệm. Chúng ta hãy tìm hiểu xem những ảnh hưởng của chiều sâu đến kết quả.

  • Khi có sự gia tăng của chiều sâu thì hiệu năng của cloneForce bị giảm đi hẳn.
  • Riêng với cloneJSON cũng giảm đi đáng kể.
  • Hiệu năng của cloneLoop thì cao hơn clone. Có thể thấy rằng thời gian thực hiện của đệ quy không có sự chênh lệch đáng kể so với việc dùng vòng lặp

Riêng cloneForce chúng ta hãy kiểm tra về hiệu năng giới hạn của phương thức này. Chúng ta sẽ kiểm tra số thời gian cần thiết để thực hiện các mức tăng dần của độ sâu.


var data1 = createData(2000, 0);
var data2 = createData(4000, 0);
var data3 = createData(6000, 0);
var data4 = createData(8000, 0);
var data5 = createData(10000, 0);

cloneForce(data1)
cloneForce(data2)
cloneForce(data3)
cloneForce(data4)
cloneForce(data5)

Qua quá trình kiểm tra, chúng ta có thể thấy rằng thời gian cần thiết để thực hiện sẽ phát triển theo cấp số nhân. Khi độ sâu đạt hơn 10000, độ trễ có thể lên tới 300ms.

Kết

Trên thực tế thì mỗi phương pháp đều sẽ có ưu và khuyết điểm riêng. Nhưng như chúng ta vẫn hay nói nhân tận kỳ tài, vật tất kỳ dụng . Tùy mỗi hoàn cảnh mà sẽ có cách sử dụng khác nhau.

Với những thông tin dưới đây, tôi hy vọng sẽ giúp được các bạn trong việc chọn lựa được phương pháp phù hợp.

clone cloneJSON cloneLoop cloneForce
Độ khó 2 sao 1 sao 3 sao 4 sao
Tương thích IE 6 IE 8 IE 6 IE 6
Tham chiếu vòng Một lớp Không hỗ trợ Một lớp Một lớp
Stack overflow Có thể xảy ra Có thể xảy ra Không xảy ra Không xảy ra
Giữ tham chiếu Không Không Không
Trường hợp phù hợp Sao chép cơ bản Sao chép cơ bản Sao chép nhiều cấp độ Giữ lại tham chiếu

Nguồn cảm hứng cho bài viết này đến từ @jsmini/clone. Nếu các bạn muốn sử dụng thử bốn phương pháp deep copy thì bạn có thể sử dụng thư viện @jsmini/clone như trong đoạn code phía dưới.


// npm install --save @jsmini/clone
import { clone, cloneJSON, cloneLoop, cloneForce } from '@jsmini/clone';

Để đảm bảo tính đơn giản và dễ đọc, bài viết này đã loại bỏ một số điều kiện. Nếu bạn muốn tìm hiểu thêm về những đoạn code đó trong thư viện. Bạn có thể đọc trong mã nguồn của @jsmini/clone.

@jsmini/clone được phát triển bởi jsmini cam kết cung cấp cho các bạn một thư viện nhỏ gọn, dễ dùng và chất lượng cao.

Sự ra đời của jsmini thì không thể tách rời khỏi jslib-base. Thật cảm kích jslib-base vì những công nghệ cơ bản được được áp dụng cho jsmini.

Rất cám ơn những bạn đã dành thời gian đọc bài biết này. Tôi tin rằng ngay lúc này các bạn có thể xử lý được kha khá các câu hỏi liên quan đến deep copy. Nếu có bất kì câu hỏi nào các bạn có thể bàn luận với tôi.

Cuối cùng, tôi cũng xin giới thiệu với các bạn cuốn sách mới của tôi React状态管理与同构实战 (React State Management and Isomorphism) để hiểu hơn về công nghệ Isomorphic. Cám ơn các bạn đã ủng hộ.

JD.com:https://item.jd.com/12403508.html

Dangdang:http://product.dangdang.com/25308679.html

Tôi cũng xin có một số thông tin về tuyển dụng frontend, backend, di động. Địa điểm làm việc Bắc Kinh, Thượng Hải, Thành Đô. Nào các sinh viên tinh anh và ưu tú, các bạn có thể gửi thông tin cho tôi qua địa chỉ email yanhaijing@yeah.net

Một số bình luận:

huangzhhui

Mục đích của một cuộc phỏng vấn không phải là loại bỏ người phỏng vấn

  • Tác giả Để tìm được người tốt trong cuộc phỏng vấn, chúng ta cần sự sàng lọc
  • flcwl Điều đó có phải là những ứng viên đã đọc rất nhiều bài viết?
  • Tác giả Mày đã biết quá nhiều (^o^)/~

CoyPan

Rất hay, nhưng có vẻ sử dụng JSON.parse(JSON.stringify(source)); sẽ làm biến mất cái function

  • Tác giả Vâng, tôi hiểu điều đó. Nhưng điều đó lại làm tôi cảm thấy trăn trở rằng liệu chúng ta có nên đặt phương thức vào trong dữ liệu?

Beats0

Hiện nay, có rất nhiều người sử dụng các thư viện có sẵn như lodash, underscore để thực những việc trên. Tác giả có suy nghĩ gì về điều này.

  • Tác giả Tất nhiên là chúng ta vẫn cứ sử dụng. Nhưng nếu bạn muốn cái thiện cái hiện tại thì trước tiên bạn phải nắm được nguyên tắc hoạt động trước đã.

Kết (Người dịch)

Đây là một bài viết khá hay mà mình tìm được. Nhan tiên sinh vẫn còn khá nhiều bài khác cũng rất chất lượng nếu có thời gian mình sẽ dịch tiếp. Hoặc nếu có thể các bạn có thể lên trang cá nhân của anh ta để đọc. Mình đã để link ở đầu bài. Thêm một phần là khác với bài viết lần trước của tác giả Huang Xuan. Bài viết này mới vừa được viết gần đây thôi nên phần tuyển dụng phía dưới (tính đến thời điểm mình đăng bài dịch này) vẫn còn tác dụng nhé. Nếu các bạn có khả năng có thể gửi email vào địa chỉ phía trên để thử sức mình ở Bắc Kinh, Thượng Hải hoặc Thành Đô.

Cuối cùng xin chân thành cảm ơn Nhan tiên sinh vì bài viết hết sức chi tiết và "có tâm" này. Các bạn có thể vào trang github của anh ta (có khá nhiều open source) để tham khảo và đừng quên "star" nếu thấy hữu ích nhé. github.com/yanhaijing

Chú thích

  • Nhân tận kỳ tài, vật tất kỳ dụng: Người tài sẽ có nơi để thể hiện, đồ vật ắt có chổ hữu dụng. Ý câu này là mỗi phương pháp hoặc con người chỉ cần được sử dụng đúng nơi đúng việc thì hết thảy đều hữu dụng. []
  • Thành cũng Tiêu Hà, bại cũng Tiêu Hà: Dự vào điển tích thời Hán Cao Tổ. Chỉ những việc mà thành bại đều có cùng một nguồn gốc.[]