Việc sử dụng các chức năng ẩn danh có ảnh hưởng đến hiệu suất không?


89

Tôi đã tự hỏi, có sự khác biệt về hiệu suất giữa việc sử dụng các hàm được đặt tên và các hàm ẩn danh trong Javascript không?

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = function() {
        // do something
    };
}

vs

function myEventHandler() {
    // do something
}

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = myEventHandler;
}

Đầu tiên là gọn gàng hơn vì nó không làm lộn xộn mã của bạn với các hàm hiếm khi được sử dụng, nhưng liệu bạn có khai báo lại hàm đó nhiều lần không?


Tôi biết nó không nằm trong câu hỏi, nhưng liên quan đến độ sạch / dễ đọc của mã, tôi nghĩ 'cách đúng đắn' nằm ở đâu đó ở giữa. "Sự lộn xộn" của các hàm cấp cao nhất hiếm khi được sử dụng gây khó chịu, nhưng mã lồng nhau cũng vậy, phụ thuộc rất nhiều vào các hàm ẩn danh được khai báo cùng dòng với lệnh gọi của chúng (nghĩ rằng callback hell của node.js). Cả cái trước và cái sau đều có thể làm cho việc gỡ lỗi / theo dõi thực thi trở nên khó khăn.
Zac B

Các bài kiểm tra hiệu suất bên dưới chạy hàm trong hàng nghìn lần lặp. Ngay cả khi bạn thấy sự khác biệt đáng kể, phần lớn các trường hợp sử dụng sẽ không thực hiện điều này theo các lần lặp lại của thứ tự đó. Do đó, tốt hơn hết hãy chọn bất cứ thứ gì phù hợp với nhu cầu của bạn và bỏ qua hiệu suất cho trường hợp cụ thể này.
người dùng

@nickf dĩ nhiên câu hỏi quá cũ, nhưng xem câu trả lời mới được cập nhật
Chandan Pasunoori

Câu trả lời:


89

Vấn đề về hiệu suất ở đây là chi phí tạo một đối tượng hàm mới ở mỗi lần lặp lại của vòng lặp chứ không phải việc bạn sử dụng một hàm ẩn danh:

for (var i = 0; i < 1000; ++i) {    
    myObjects[i].onMyEvent = function() {
        // do something    
    };
}

Bạn đang tạo một nghìn đối tượng hàm riêng biệt mặc dù chúng có cùng nội dung mã và không có ràng buộc với phạm vi từ vựng ( đóng ). Mặt khác, cách sau có vẻ nhanh hơn vì nó chỉ đơn giản gán cùng một tham chiếu hàm cho các phần tử mảng trong suốt vòng lặp:

function myEventHandler() {
    // do something
}

for (var i = 0; i < 1000; ++i) {
    myObjects[i].onMyEvent = myEventHandler;
}

Nếu bạn đã tạo hàm ẩn danh trước khi vào vòng lặp, sau đó chỉ gán các tham chiếu đến nó cho các phần tử mảng trong khi bên trong vòng lặp, bạn sẽ thấy rằng không có sự khác biệt về hiệu suất hoặc ngữ nghĩa khi so sánh với phiên bản hàm được đặt tên:

var handler = function() {
    // do something    
};
for (var i = 0; i < 1000; ++i) {    
    myObjects[i].onMyEvent = handler;
}

Nói tóm lại, không có chi phí hiệu suất có thể quan sát được khi sử dụng các hàm ẩn danh được đặt tên.

Ngoài ra, có thể nhìn từ trên xuống rằng không có sự khác biệt giữa:

function myEventHandler() { /* ... */ }

và:

var myEventHandler = function() { /* ... */ }

Cái trước là một khai báo hàm trong khi cái sau là một phép gán biến cho một hàm ẩn danh. Mặc dù chúng có thể có cùng tác dụng, nhưng JavaScript xử lý chúng hơi khác một chút. Để hiểu sự khác biệt, tôi khuyên bạn nên đọc “Sự mơ hồ về khai báo hàm JavaScript ”.

Thời gian thực thi thực tế cho bất kỳ cách tiếp cận nào phần lớn sẽ được quyết định bởi việc triển khai trình biên dịch và thời gian chạy của trình duyệt. Để có so sánh đầy đủ về hiệu suất của trình duyệt hiện đại, hãy truy cập trang web JS Perf


Bạn quên dấu ngoặc trước thân hàm. Tôi chỉ thử nghiệm nó, họ được yêu cầu.
Chinoto Vokro

có vẻ như kết quả điểm chuẩn rất phụ thuộc vào js-engine!
aleclofabbro

3
Không có sai sót trong ví dụ JS Perf: Trường hợp 1 chỉ xác định hàm, trong khi trường hợp 2 & 3 dường như vô tình gọi hàm.
bluenote10

Vì vậy, sử dụng lý luận này, có nghĩa là khi phát triển node.jscác ứng dụng web, tốt hơn nên tạo các chức năng bên ngoài luồng yêu cầu và chuyển chúng dưới dạng lệnh gọi lại, hơn là tạo lệnh gọi lại ẩn danh?
Xavier T Mukodi

23

Đây là mã thử nghiệm của tôi:

var dummyVar;
function test1() {
    for (var i = 0; i < 1000000; ++i) {
        dummyVar = myFunc;
    }
}

function test2() {
    for (var i = 0; i < 1000000; ++i) {
        dummyVar = function() {
            var x = 0;
            x++;
        };
    }
}

function myFunc() {
    var x = 0;
    x++;
}

document.onclick = function() {
    var start = new Date();
    test1();
    var mid = new Date();
    test2();
    var end = new Date();
    alert ("Test 1: " + (mid - start) + "\n Test 2: " + (end - mid));
}

Kết quả:
Thử nghiệm 1: 142ms Thử nghiệm 2: 1983ms

Có vẻ như công cụ JS không nhận ra rằng nó cùng một chức năng trong Test2 và biên dịch nó mỗi lần.


3
Thử nghiệm này được thực hiện trên trình duyệt nào?
andynil,

5
Times cho tôi trên Chrome 23: (2ms / 17ms), IE9: (20ms / 83ms), FF 17: (2ms / 96ms)
Davy8

Câu trả lời của bạn xứng đáng có trọng lượng hơn. Thời gian của tôi trên Intel i5 4570S: Chrome 41 (1/9), IE11 (1/25), FF36 (1/14). Rõ ràng là chức năng ẩn danh trong một vòng lặp hoạt động kém hơn.
ThisClark,

3
Thử nghiệm này không hữu ích như nó xuất hiện. Trong cả hai ví dụ đều không thực sự thực thi chức năng nội thất. Hiệu quả mà tất cả thử nghiệm này cho thấy là tạo một hàm 10000000 lần nhanh hơn tạo một hàm một lần.
Nucleon

2

Theo nguyên tắc thiết kế chung, bạn nên tránh đặt nhiều lần cùng một mã. Thay vào đó, bạn nên nâng mã phổ biến ra thành một hàm và thực thi hàm đó (chung, đã được kiểm tra tốt, dễ sửa đổi) từ nhiều nơi.

Nếu (không giống như những gì bạn suy luận từ câu hỏi của mình) bạn đang khai báo hàm nội bộ một lần và sử dụng mã đó một lần (và không có gì khác giống trong chương trình của bạn) thì một hàm ẩn danh có thể (đó là một người đoán) được xử lý theo cách tương tự bởi trình biên dịch như một hàm được đặt tên bình thường.

Nó là một tính năng rất hữu ích trong các trường hợp cụ thể, nhưng không nên được sử dụng trong nhiều trường hợp.


1

Tôi sẽ không mong đợi nhiều sự khác biệt nhưng nếu có thì nó có thể sẽ khác nhau tùy theo công cụ tạo tập lệnh hoặc trình duyệt.

Nếu bạn thấy mã dễ tìm hiểu hơn, hiệu suất không phải là vấn đề trừ khi bạn muốn gọi hàm hàng triệu lần.


1

Nơi chúng ta có thể có tác động đến hiệu suất là trong hoạt động khai báo các hàm. Đây là điểm chuẩn của việc khai báo các hàm bên trong ngữ cảnh của một hàm khác hoặc bên ngoài:

http://jsperf.com/ Chức năng-context-benchmark

Trong Chrome thao tác sẽ nhanh hơn nếu chúng ta khai báo hàm bên ngoài, nhưng trên Firefox thì ngược lại.

Trong ví dụ khác, chúng ta thấy rằng nếu hàm bên trong không phải là một hàm thuần túy, thì nó cũng sẽ thiếu hiệu suất trong Firefox: http://jsperf.com/ Chức năng- context-benchmark-3


0

Điều chắc chắn sẽ làm cho vòng lặp của bạn nhanh hơn trên nhiều trình duyệt, đặc biệt là trình duyệt IE, đang lặp lại như sau:

for (var i = 0, iLength = imgs.length; i < iLength; i++)
{
   // do something
}

Bạn đã đặt 1000 tùy ý vào điều kiện vòng lặp, nhưng bạn sẽ hiểu được sự sai lệch của tôi nếu bạn muốn xem qua tất cả các mục trong mảng.


0

một tham chiếu gần như luôn luôn chậm hơn so với thứ mà nó tham chiếu đến. Hãy nghĩ theo cách này - giả sử bạn muốn in kết quả của việc cộng 1 + 1. Điều này có ý nghĩa hơn:

alert(1 + 1);

hoặc là

a = 1;
b = 1;
alert(a + b);

Tôi nhận ra đó là một cách thực sự đơn giản để nhìn nó, nhưng nó mang tính minh họa, phải không? Chỉ sử dụng một tham chiếu nếu nó sẽ được sử dụng nhiều lần - ví dụ: ví dụ nào trong số những ví dụ này có ý nghĩa hơn:

$(a.button1).click(function(){alert('you clicked ' + this);});
$(a.button2).click(function(){alert('you clicked ' + this);});

hoặc là

function buttonClickHandler(){alert('you clicked ' + this);}
$(a.button1).click(buttonClickHandler);
$(a.button2).click(buttonClickHandler);

Cách thứ hai là thực hành tốt hơn, ngay cả khi nó có nhiều dòng hơn. Hy vọng rằng tất cả những điều này là hữu ích. (và cú pháp jquery không làm bất cứ ai bị loại bỏ)


0

@nickf

(ước gì tôi có người đại diện để chỉ bình luận, nhưng tôi chỉ tìm thấy trang web này)

Quan điểm của tôi là có sự nhầm lẫn ở đây giữa các hàm được đặt tên / ẩn danh và trường hợp sử dụng thực thi + biên dịch trong một lần lặp. Như tôi đã minh họa, sự khác biệt giữa anon + được đặt tên là không đáng kể - tôi đang nói rằng đó là trường hợp sử dụng bị lỗi.

Nó có vẻ hiển nhiên với tôi, nhưng nếu không, tôi nghĩ lời khuyên tốt nhất là "đừng làm những điều ngu ngốc" (trong đó chuyển khối liên tục + tạo đối tượng trong trường hợp sử dụng này là một) và nếu bạn không chắc chắn, hãy kiểm tra!


0

ĐÚNG! Các chức năng ẩn danh nhanh hơn các chức năng thông thường. Có lẽ nếu tốc độ là quan trọng hàng đầu ... quan trọng hơn việc sử dụng lại mã thì hãy xem xét sử dụng các hàm ẩn danh.

Có một bài viết rất hay về việc tối ưu hóa javascript và các hàm ẩn danh ở đây:

http://dev.opera.com/articles/view/enough-javascript/?page=2


0

Đối tượng ẩn danh nhanh hơn đối tượng được đặt tên. Nhưng việc gọi nhiều hàm hơn thì đắt hơn và ở một mức độ nào đó làm lu mờ mọi khoản tiết kiệm mà bạn có thể nhận được từ việc sử dụng các hàm ẩn danh. Mỗi hàm được gọi sẽ thêm vào ngăn xếp cuộc gọi, điều này giới thiệu một lượng chi phí nhỏ nhưng không đáng kể.

Nhưng trừ khi bạn đang viết quy trình mã hóa / giải mã hoặc thứ gì đó tương tự nhạy cảm với hiệu suất, như nhiều người khác đã lưu ý rằng tốt hơn hết là tối ưu hóa cho mã thanh lịch, dễ đọc hơn mã nhanh.

Giả sử bạn đang viết mã được cấu trúc tốt, thì các vấn đề về tốc độ sẽ là trách nhiệm của những người viết trình thông dịch / trình biên dịch.


0

@nickf

Tuy nhiên, đó là một thử nghiệm khá phức tạp, bạn đang so sánh thời gian thực thi và biên dịch ở đó, điều này rõ ràng sẽ tốn phương pháp 1 (biên dịch N lần, tùy thuộc vào công cụ JS) với phương pháp 2 (biên dịch một lần). Tôi không thể tưởng tượng một nhà phát triển JS sẽ vượt qua thử thách viết mã của họ theo cách như vậy.

Một cách tiếp cận thực tế hơn nhiều là chuyển nhượng ẩn danh, như trên thực tế bạn đang sử dụng cho document của mình. Phương thức nhấp chuột giống như sau hơn, thực tế hơi ủng hộ phương pháp anon.

Sử dụng khung thử nghiệm tương tự như của bạn:


function test(m)
{
    for (var i = 0; i < 1000000; ++i) 
    {
        m();
    }
}

function named() {var x = 0; x++;}

var test1 = named;

var test2 = function() {var x = 0; x++;}

document.onclick = function() {
    var start = new Date();
    test(test1);
    var mid = new Date();
    test(test2);
    var end = new Date();
    alert ("Test 1: " + (mid - start) + "ms\n Test 2: " + (end - mid) + "ms");
}

0

Như đã chỉ ra trong các nhận xét cho câu trả lời @nickf: Câu trả lời cho

Tạo một chức năng nhanh hơn một lần so với tạo một triệu lần

chỉ đơn giản là có. Nhưng như JS perf của anh ấy cho thấy, nó không chậm hơn một phần triệu, cho thấy rằng nó thực sự nhanh hơn theo thời gian.

Câu hỏi thú vị hơn đối với tôi là:

Làm thế nào để tạo + chạy lặp lại so với tạo một lần + chạy lặp lại .

Nếu một hàm thực hiện một phép tính phức tạp thì thời gian để tạo đối tượng hàm rất có thể là không đáng kể. Nhưng còn over head của tạo trong trường hợp chạy nhanh thì sao? Ví dụ:

// Variant 1: create once
function adder(a, b) {
  return a + b;
}
for (var i = 0; i < 100000; ++i) {
  var x = adder(412, 123);
}

// Variant 2: repeated creation via function statement
for (var i = 0; i < 100000; ++i) {
  function adder(a, b) {
    return a + b;
  }
  var x = adder(412, 123);
}

// Variant 3: repeated creation via function expression
for (var i = 0; i < 100000; ++i) {
  var x = (function(a, b) { return a + b; })(412, 123);
}

Đây JS Perf cho thấy rằng việc tạo ra các chức năng chỉ một lần được nhanh như mong đợi. Tuy nhiên, ngay cả với một thao tác rất nhanh như một phép thêm đơn giản, chi phí tạo hàm lặp đi lặp lại chỉ là một vài phần trăm.

Sự khác biệt có lẽ chỉ trở nên đáng kể trong trường hợp việc tạo đối tượng hàm phức tạp, trong khi vẫn duy trì thời gian chạy không đáng kể, ví dụ, nếu toàn bộ thân hàm được bao bọc thành một if (unlikelyCondition) { ... }.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.