JavaScript

Giải thích ủy quyền sự kiện

Ủy quyền sự kiện là một kỹ thuật bao gồm việc thêm trình lắng nghe sự kiện vào một phần tử cha thay vì thêm chúng vào các phần tử con cháu. Trình lắng nghe sẽ kích hoạt bất cứ khi nào sự kiện được kích hoạt trên các phần tử con cháu do sự kiện nổi lên trong DOM. Lợi ích của kỹ thuật này là:

  • Dấu chân bộ nhớ giảm vì chỉ cần một trình xử lý duy nhất trên phần tử cha, thay vì phải gắn trình xử lý sự kiện trên mỗi phần tử con cháu.
  • Không cần phải hủy liên kết trình xử lý khỏi các phần tử bị xóa và liên kết sự kiện cho các phần tử mới.

Giải thích cách 'this' hoạt động trong JavaScript

Không có lời giải thích đơn giản cho 'this'; nó là một trong những khái niệm khó hiểu nhất trong JavaScript. Một lời giải thích chung chung là giá trị của 'this' phụ thuộc vào cách hàm được gọi. Tôi đã đọc nhiều lời giải thích về 'this' trực tuyến, và tôi thấy lời giải thích của [Arnav Aggrawal] là rõ ràng nhất. Các quy tắc sau được áp dụng:

  1. Nếu từ khóa 'new' được sử dụng khi gọi hàm, 'this' bên trong hàm là một đối tượng hoàn toàn mới.
  2. Nếu 'apply', 'call' hoặc 'bind' được sử dụng để gọi/tạo một hàm, 'this' bên trong hàm là đối tượng được truyền vào làm đối số.
  3. Nếu một hàm được gọi như một phương thức, chẳng hạn như 'obj.method()' — 'this' là đối tượng mà hàm là một thuộc tính của nó.
  4. Nếu một hàm được gọi như một hàm tự do, nghĩa là nó được gọi mà không có bất kỳ điều kiện nào ở trên, 'this' là đối tượng toàn cục. Trong trình duyệt, đó là đối tượng 'window'. Nếu ở chế độ nghiêm ngặt ('use strict'), 'this' sẽ là 'undefined' thay vì đối tượng toàn cục.
  5. Nếu nhiều quy tắc ở trên được áp dụng, quy tắc cao hơn sẽ thắng và sẽ đặt giá trị 'this'.
  6. Nếu hàm là một hàm mũi tên ES2015, nó bỏ qua tất cả các quy tắc trên và nhận giá trị 'this' của phạm vi bao quanh nó tại thời điểm nó được tạo.

Để có lời giải thích chi tiết, hãy xem [bài viết của anh ấy trên Medium].

Bạn có thể đưa ra một ví dụ về một trong những cách mà việc làm việc với điều này đã thay đổi trong ES6 không?

ES6 cho phép bạn sử dụng [hàm mũi tên] sử dụng [phạm vi từ vựng bao quanh]. Điều này thường tiện lợi, nhưng ngăn người gọi kiểm soát ngữ cảnh thông qua '.call' hoặc '.apply' — hậu quả là một thư viện như 'jQuery' sẽ không liên kết đúng 'this' trong các hàm xử lý sự kiện của bạn. Do đó, điều quan trọng là phải ghi nhớ điều này khi tái cấu trúc các ứng dụng kế thừa lớn.

Giải thích cách kế thừa nguyên mẫu hoạt động

Tất cả các đối tượng JavaScript đều có một thuộc tính 'proto' ngoại trừ các đối tượng được tạo bằng 'Object.create(null)', đó là một tham chiếu đến một đối tượng khác, được gọi là "nguyên mẫu" của đối tượng. Khi một thuộc tính được truy cập trên một đối tượng và nếu thuộc tính đó không được tìm thấy trên đối tượng đó, công cụ JavaScript sẽ tìm kiếm thuộc tính 'proto' của đối tượng, và 'proto' của 'proto' và cứ thế, cho đến khi nó tìm thấy thuộc tính được định nghĩa trên một trong các 'proto' hoặc cho đến khi nó đạt đến cuối chuỗi nguyên mẫu. Hành vi này mô phỏng kế thừa cổ điển, nhưng thực sự nó giống [ủy quyền hơn là kế thừa].

Ví dụ về Kế thừa Nguyên mẫu

// Hàm tạo đối tượng cha. function Animal(name) { this.name = name; } // Thêm một phương thức vào nguyên mẫu của đối tượng cha. Animal.prototype.makeSound = function () { console.log('The ' + this.constructor.name + ' makes a sound.'); }; // Hàm tạo đối tượng con. function Dog(name) { Animal.call(this, name); // Gọi hàm tạo cha. } // Đặt nguyên mẫu của đối tượng con là nguyên mẫu của cha. Object.setPrototypeOf(Dog.prototype, Animal.prototype); // Thêm một phương thức vào nguyên mẫu của đối tượng con. Dog.prototype.bark = function () { console.log('Woof!'); }; // Tạo một thể hiện mới của Dog. const bolt = new Dog('Bolt'); // Gọi các phương thức trên đối tượng con. console.log(bolt.name); // "Bolt" bolt.makeSound(); // "The Dog makes a sound." bolt.bark(); // "Woof!"

Những điều cần lưu ý là:

  • '.makeSound' không được định nghĩa trên 'Dog', vì vậy công cụ đi lên chuỗi nguyên mẫu và tìm thấy '.makeSound' từ 'Animal' được kế thừa.
  • Việc sử dụng 'Object.create' để xây dựng chuỗi kế thừa không còn được khuyến nghị. Thay vào đó hãy sử dụng 'Object.setPrototypeOf'.

Bạn nghĩ gì về AMD so với CommonJS?

Cả hai đều là cách triển khai một hệ thống mô-đun, vốn không có sẵn trong JavaScript cho đến khi ES2015 xuất hiện. CommonJS là đồng bộ trong khi AMD (Asynchronous Module Definition) rõ ràng là không đồng bộ. CommonJS được thiết kế với mục đích phát triển phía máy chủ trong khi AMD, với sự hỗ trợ tải mô-đun không đồng bộ, được dành nhiều hơn cho trình duyệt.

Tôi thấy cú pháp AMD khá dài dòng và CommonJS gần hơn với kiểu bạn sẽ viết câu lệnh import trong các ngôn ngữ khác. Hầu hết thời gian, tôi thấy AMD không cần thiết, bởi vì nếu bạn phục vụ tất cả JavaScript của mình vào một tệp gói được nối lại, bạn sẽ không hưởng lợi từ các thuộc tính tải không đồng bộ. Ngoài ra, cú pháp CommonJS gần hơn với kiểu viết mô-đun của Node và có ít chi phí chuyển đổi ngữ cảnh hơn khi chuyển đổi giữa phát triển JavaScript phía máy khách và phía máy chủ.

Tôi rất vui vì với các mô-đun ES2015, có hỗ trợ cả tải đồng bộ và không đồng bộ, cuối cùng chúng ta có thể chỉ bám vào một cách tiếp cận. Mặc dù nó chưa được triển khai hoàn chỉnh trong trình duyệt và trong Node, chúng ta luôn có thể sử dụng các trình biên dịch để chuyển đổi mã của mình.

Giải thích tại sao phần sau không hoạt động như một IIFE: 'function foo(){ }();'. Cần thay đổi gì để biến nó thành một IIFE đúng cách?

IIFE là viết tắt của Immediately Invoked Function Expressions (Biểu thức hàm được gọi ngay lập tức). Trình phân tích cú pháp JavaScript đọc 'function foo(){ }();' là 'function foo(){ }' và '();', trong đó phần trước là một khai báo hàm và phần sau (một cặp dấu ngoặc đơn) là một nỗ lực gọi một hàm nhưng không có tên được chỉ định, do đó nó ném lỗi 'Uncaught SyntaxError: Unexpected token )'.

Đây là hai cách để khắc phục nó bằng cách thêm nhiều dấu ngoặc đơn hơn: '(function foo(){ })()' và '(function foo(){ }())'. Các câu lệnh bắt đầu bằng 'function' được coi là khai báo hàm; bằng cách gói hàm này trong '()', nó trở thành một biểu thức hàm sau đó có thể được thực thi bằng '()' tiếp theo. Các hàm này không được hiển thị trong phạm vi toàn cục và bạn thậm chí có thể bỏ qua tên của nó nếu bạn không cần tham chiếu chính nó trong phần thân.

Bạn cũng có thể sử dụng toán tử 'void': 'void function foo(){ }();'. Thật không may, có một vấn đề với cách tiếp cận như vậy. Việc đánh giá biểu thức đã cho luôn là 'undefined', vì vậy nếu hàm IIFE của bạn trả về bất cứ thứ gì, bạn không thể sử dụng nó. Một ví dụ:

const foo = void (function bar() { return 'foo'; })(); console.log(foo); // undefined

Sự khác biệt giữa biến 'null', 'undefined' và chưa khai báo là gì? Bạn sẽ kiểm tra bất kỳ trạng thái nào trong số này như thế nào?

Các biến chưa khai báo được tạo khi bạn gán một giá trị cho một định danh chưa được tạo trước đó bằng 'var', 'let' hoặc 'const'. Các biến chưa khai báo sẽ được định nghĩa toàn cục, bên ngoài phạm vi hiện tại. Trong chế độ nghiêm ngặt, một 'ReferenceError' sẽ được ném ra khi bạn cố gắng gán cho một biến chưa khai báo. Các biến chưa khai báo tệ giống như các biến toàn cục tệ. Tránh chúng bằng mọi giá! Để kiểm tra chúng, hãy gói việc sử dụng chúng trong một khối 'try'/'catch'.

function foo() { x = 1; // Ném ra ReferenceError trong chế độ nghiêm ngặt } foo(); console.log(x); // 1

Một biến là 'undefined' là một biến đã được khai báo, nhưng chưa được gán giá trị. Nó có kiểu 'undefined'. Nếu một hàm không trả về bất kỳ giá trị nào là kết quả của việc thực thi nó được gán cho một biến, biến đó cũng có giá trị là 'undefined'. Để kiểm tra nó, hãy so sánh bằng cách sử dụng toán tử đẳng thức nghiêm ngặt ('===') hoặc 'typeof' sẽ cho chuỗi ''undefined''. Lưu ý rằng bạn không nên sử dụng toán tử đẳng thức trừu tượng để kiểm tra, vì nó cũng sẽ trả về 'true' nếu giá trị là 'null'.

var foo; console.log(foo); // undefined console.log(foo === undefined); // true console.log(typeof foo === 'undefined'); // true console.log(foo == null); // true. Sai, đừng dùng cái này để kiểm tra! function bar() {} var baz = bar(); console.log(baz); // undefined

Một biến là 'null' sẽ được gán rõ ràng giá trị 'null'. Nó đại diện cho không có giá trị và khác với 'undefined' ở chỗ nó đã được gán rõ ràng. Để kiểm tra 'null', chỉ cần so sánh bằng cách sử dụng toán tử đẳng thức nghiêm ngặt. Lưu ý rằng như trên, bạn không nên sử dụng toán tử đẳng thức trừu tượng ('==') để kiểm tra, vì nó cũng sẽ trả về 'true' nếu giá trị là 'undefined'.

var foo = null; console.log(foo === null); // true console.log(typeof foo === 'object'); // true console.log(foo == undefined); // true. Sai, đừng dùng cái này để kiểm tra!

Là một thói quen cá nhân, tôi không bao giờ để các biến của mình chưa khai báo hoặc chưa gán. Tôi sẽ gán rõ ràng 'null' cho chúng sau khi khai báo nếu tôi chưa có ý định sử dụng nó. Nếu bạn sử dụng một công cụ kiểm tra cú pháp trong quy trình làm việc của mình, nó thường cũng có thể kiểm tra xem bạn có đang tham chiếu các biến chưa khai báo hay không.

Closure là gì và bạn sẽ sử dụng nó như thế nào/tại sao?

Closure là sự kết hợp giữa một hàm và môi trường từ vựng mà hàm đó được khai báo. Từ "lexical" (từ vựng) đề cập đến thực tế rằng phạm vi từ vựng sử dụng vị trí mà một biến được khai báo trong mã nguồn để xác định vị trí biến đó có sẵn. Closure là các hàm có quyền truy cập vào các biến của hàm bên ngoài (bao quanh) – chuỗi phạm vi ngay cả sau khi hàm bên ngoài đã trả về.

Tại sao bạn lại sử dụng một cái?

  • Bảo mật dữ liệu / mô phỏng các phương thức riêng tư với closure. Thường được sử dụng trong [mẫu mô-đun].
  • [Ứng dụng một phần hoặc currying].

Bạn có thể mô tả sự khác biệt chính giữa vòng lặp '.forEach' và vòng lặp '.map()' và tại sao bạn lại chọn cái này thay vì cái kia không?

Để hiểu sự khác biệt giữa hai cái, hãy xem mỗi hàm làm gì.

forEach

  • Lặp qua các phần tử trong một mảng.
  • Thực thi một hàm gọi lại cho mỗi phần tử.
  • Không trả về giá trị.
const a = [1, 2, 3]; const doubled = a.forEach((num, index) => { // Làm gì đó với num và/hoặc index. }); // doubled = undefined

map

  • Lặp qua các phần tử trong một mảng.
  • "Ánh xạ" mỗi phần tử sang một phần tử mới bằng cách gọi hàm trên mỗi phần tử, tạo ra một mảng mới làm kết quả.
const a = [1, 2, 3]; const doubled = a.map((num) => { return num * 2; }); // doubled = [2, 4, 6]

Sự khác biệt chính giữa '.forEach' và '.map()' là '.map()' trả về một mảng mới. Nếu bạn cần kết quả, nhưng không muốn thay đổi mảng gốc, '.map()' là lựa chọn rõ ràng. Nếu bạn chỉ cần lặp qua một mảng, 'forEach' là một lựa chọn tốt.

Trường hợp sử dụng điển hình cho các hàm ẩn danh là gì?

Chúng có thể được sử dụng trong IIFE để đóng gói một số mã trong một phạm vi cục bộ để các biến được khai báo trong đó không bị rò rỉ ra phạm vi toàn cục.

(function () { // Một số mã ở đây. })();

Như một callback được sử dụng một lần và không cần sử dụng ở bất kỳ nơi nào khác. Mã sẽ trông tự chứa và dễ đọc hơn khi các trình xử lý được định nghĩa ngay trong mã gọi chúng, thay vì phải tìm kiếm ở nơi khác để tìm thân hàm.

setTimeout(function () { console.log('Hello world!'); }, 1000);

Các đối số cho các cấu trúc lập trình hàm hoặc Lodash (tương tự như callbacks).

const arr = [1, 2, 3]; const double = arr.map(function (el) { return el * 2; }); console.log(double); // [2, 4, 6]

Bạn tổ chức mã của mình như thế nào? (mẫu mô-đun, kế thừa cổ điển?)

Trước đây, tôi đã sử dụng Backbone cho các mô hình của mình, khuyến khích cách tiếp cận hướng đối tượng hơn, tạo các mô hình Backbone và gắn các phương thức vào chúng.

Mẫu mô-đun vẫn tuyệt vời, nhưng ngày nay, tôi sử dụng React/Redux, sử dụng luồng dữ liệu một chiều dựa trên kiến trúc Flux. Tôi sẽ đại diện cho các mô hình ứng dụng của mình bằng các đối tượng đơn giản và viết các hàm thuần tiện ích để thao tác các đối tượng này. Trạng thái được thao tác bằng cách sử dụng các hành động và bộ giảm tốc như trong bất kỳ ứng dụng Redux nào khác.

Tôi tránh sử dụng kế thừa cổ điển nếu có thể. Khi và nếu tôi làm, tôi tuân thủ [các quy tắc này].

Sự khác biệt giữa đối tượng host và đối tượng native là gì?

Các đối tượng native là các đối tượng là một phần của ngôn ngữ JavaScript được định nghĩa bởi đặc tả ECMAScript, chẳng hạn như 'String', 'Math', 'RegExp', 'Object', 'Function', v.v.

Các đối tượng host được cung cấp bởi môi trường thời gian chạy (trình duyệt hoặc Node), chẳng hạn như 'window', 'XMLHTTPRequest', v.v.

Sự khác biệt giữa: 'function Person(){}', 'var person = Person()', và 'var person = new Person()''?

Câu hỏi này khá mơ hồ. Đoán tốt nhất của tôi về ý định của nó là nó đang hỏi về các hàm tạo trong JavaScript. Nói về mặt kỹ thuật, 'function Person(){}' chỉ là một khai báo hàm bình thường. Quy ước là sử dụng PascalCase cho các hàm được dùng làm hàm tạo.

'var person = Person()' gọi 'Person' như một hàm, chứ không phải như một hàm tạo. Việc gọi như vậy là một lỗi phổ biến nếu hàm được dùng làm hàm tạo. Thông thường, hàm tạo không trả về bất kỳ giá trị nào, do đó, việc gọi hàm tạo như một hàm bình thường sẽ trả về 'undefined' và giá trị đó được gán cho biến được dùng làm thể hiện.

'var person = new Person()' tạo một thể hiện của đối tượng 'Person' bằng cách sử dụng toán tử 'new', kế thừa từ 'Person.prototype'. Một cách khác là sử dụng 'Object.create', chẳng hạn như: 'Object.create(Person.prototype)'.

function Person(name) { this.name = name; } var person = Person('John'); console.log(person); // undefined console.log(person.name); // Uncaught TypeError: Cannot read property 'name' of undefined var person = new Person('John'); console.log(person); // Person { name: "John" } console.log(person.name); // "john"

Sự khác biệt giữa '.call' và '.apply' là gì?

Cả '.call' và '.apply' đều được sử dụng để gọi các hàm và tham số đầu tiên sẽ được sử dụng làm giá trị của 'this' trong hàm. Tuy nhiên, '.call' nhận các đối số được phân tách bằng dấu phẩy làm các đối số tiếp theo trong khi '.apply' nhận một mảng các đối số làm đối số tiếp theo. Một cách dễ dàng để ghi nhớ điều này là C cho 'call' và các đối số được phân tách bằng dấu phẩy và A cho 'apply' và một mảng các đối số.

function add(a, b) { return a + b; } console.log(add.call(null, 1, 2)); // 3 console.log(add.apply(null, [1, 2])); // 3

Giải thích 'Function.prototype.bind'.

Trích nguyên văn từ [MDN]:

Phương thức 'bind()' tạo ra một hàm mới mà, khi được gọi, từ khóa 'this' của nó được đặt thành giá trị được cung cấp, với một chuỗi đối số đã cho đứng trước bất kỳ đối số nào được cung cấp khi hàm mới được gọi.

Theo kinh nghiệm của tôi, nó hữu ích nhất để liên kết giá trị của 'this' trong các phương thức của các lớp mà bạn muốn truyền vào các hàm khác. Điều này thường được thực hiện trong các thành phần React.

Khi nào bạn sẽ sử dụng 'document.write()''?

'document.write()' ghi một chuỗi văn bản vào một luồng tài liệu được mở bởi 'document.open()'. Khi 'document.write()' được thực thi sau khi trang đã tải, nó sẽ gọi 'document.open' sẽ xóa toàn bộ tài liệu ('<head>' và '<body>' bị xóa!) và thay thế nội dung bằng giá trị tham số đã cho. Do đó, nó thường được coi là nguy hiểm và dễ bị lạm dụng.

Có một số câu trả lời trực tuyến giải thích rằng 'document.write()' được sử dụng trong mã phân tích hoặc [khi bạn muốn bao gồm các kiểu chỉ hoạt động nếu JavaScript được bật]. Nó thậm chí còn được sử dụng trong HTML5 boilerplate để [tải tập lệnh song song và duy trì thứ tự thực thi]! Tuy nhiên, tôi nghi ngờ những lý do đó có thể đã lỗi thời và trong thời hiện đại, chúng có thể đạt được mà không cần sử dụng 'document.write()'. Xin hãy sửa lại tôi nếu tôi sai về điều này.

Sự khác biệt giữa phát hiện tính năng, suy luận tính năng và sử dụng chuỗi UA là gì?

Phát hiện tính năng

Phát hiện tính năng liên quan đến việc tìm hiểu xem trình duyệt có hỗ trợ một khối mã nhất định hay không và chạy mã khác tùy thuộc vào việc nó có (hoặc không), để trình duyệt luôn có thể cung cấp trải nghiệm hoạt động thay vì bị lỗi/gây lỗi trong một số trình duyệt. Ví dụ:

if ('geolocation' in navigator) { // Có thể sử dụng navigator.geolocation } else { // Xử lý việc thiếu tính năng }

[Modernizr] là một thư viện tuyệt vời để xử lý phát hiện tính năng.

Suy luận tính năng

Suy luận tính năng kiểm tra một tính năng giống như phát hiện tính năng, nhưng sử dụng một hàm khác vì nó cho rằng nó cũng sẽ tồn tại, ví dụ:

if (document.getElementsByTagName) { element = document.getElementById(id); }

Điều này thực sự không được khuyến nghị. Phát hiện tính năng đáng tin cậy hơn.

Chuỗi UA

Đây là một chuỗi do trình duyệt báo cáo cho phép các đối tác giao thức mạng xác định loại ứng dụng, hệ điều hành, nhà cung cấp phần mềm hoặc phiên bản phần mềm của tác nhân người dùng phần mềm yêu cầu. Nó có thể được truy cập thông qua 'navigator.userAgent'. Tuy nhiên, chuỗi này khó phân tích và có thể bị giả mạo. Ví dụ, Chrome báo cáo cả Chrome và Safari. Vì vậy, để phát hiện Safari, bạn phải kiểm tra chuỗi Safari và không có chuỗi Chrome. Tránh phương pháp này.

Giải thích Ajax chi tiết nhất có thể.

Ajax (asynchronous JavaScript and XML) là một tập hợp các kỹ thuật phát triển web sử dụng nhiều công nghệ web ở phía máy khách để tạo các ứng dụng web không đồng bộ. Với Ajax, các ứng dụng web có thể gửi và truy xuất dữ liệu từ máy chủ một cách không đồng bộ (trong nền) mà không ảnh hưởng đến hiển thị và hành vi của trang hiện có. Bằng cách tách lớp trao đổi dữ liệu khỏi lớp trình bày, Ajax cho phép các trang web, và mở rộng ra là các ứng dụng web, thay đổi nội dung một cách động mà không cần tải lại toàn bộ trang. Trên thực tế, các triển khai hiện đại thường sử dụng JSON thay vì XML, do ưu điểm của JSON là nguyên bản trong JavaScript.

API 'XMLHttpRequest' thường được sử dụng cho giao tiếp không đồng bộ hoặc ngày nay, API 'fetch()'.

Ưu và nhược điểm của việc sử dụng Ajax là gì?

Ưu điểm

  • Khả năng tương tác tốt hơn. Nội dung mới từ máy chủ có thể được thay đổi động mà không cần tải lại toàn bộ trang.
  • Giảm kết nối đến máy chủ vì các tập lệnh và bảng kiểu chỉ cần được yêu cầu một lần.
  • Trạng thái có thể được duy trì trên một trang. Các biến JavaScript và trạng thái DOM sẽ tồn tại vì trang chứa chính không được tải lại.
  • Về cơ bản hầu hết các ưu điểm của một SPA.

Nhược điểm

  • Các trang web động khó đánh dấu trang hơn.
  • Không hoạt động nếu JavaScript đã bị tắt trong trình duyệt.
  • Một số trình thu thập thông tin web không thực thi JavaScript và sẽ không thấy nội dung đã được tải bằng JavaScript.
  • Các trang web sử dụng Ajax để tìm nạp dữ liệu có thể sẽ phải kết hợp dữ liệu từ xa đã tìm nạp với các mẫu phía máy khách để cập nhật DOM. Để điều này xảy ra, JavaScript sẽ phải được phân tích cú pháp và thực thi trên trình duyệt, và các thiết bị di động cấp thấp có thể gặp khó khăn với điều này.
  • Về cơ bản hầu hết các nhược điểm của một SPA.

Giải thích cách JSONP hoạt động (và tại sao nó không thực sự là Ajax).

JSONP (JSON with Padding) là một phương pháp thường được sử dụng để bỏ qua các chính sách liên miền trong trình duyệt web vì các yêu cầu Ajax từ trang hiện tại đến một miền khác không được phép.

JSONP hoạt động bằng cách tạo yêu cầu đến một miền khác thông qua thẻ '<script>' và thường có tham số truy vấn 'callback', ví dụ: 'https://example.com?callback=printData'. Máy chủ sau đó sẽ gói dữ liệu trong một hàm có tên 'printData' và trả về cho máy khách.

<script> function printData(data) { console.log(`My name is ${data.name}!`); } </script> <script src="[https://example.com?callback=printData](https://example.com?callback=printData)"></script>
printData({ name: 'Yang Shun' });

Máy khách phải có hàm 'printData' trong phạm vi toàn cục của nó và hàm sẽ được máy khách thực thi khi nhận được phản hồi từ miền khác.

JSONP có thể không an toàn và có một số tác động bảo mật. Vì JSONP thực sự là JavaScript, nó có thể làm mọi thứ mà JavaScript có thể làm, vì vậy bạn cần tin tưởng nhà cung cấp dữ liệu JSONP.

Ngày nay, [CORS] là cách tiếp cận được khuyến nghị và JSONP được coi là một thủ thuật.

Bạn đã bao giờ sử dụng mẫu JavaScript chưa? Nếu có, bạn đã sử dụng thư viện nào?

Có. Handlebars, Underscore, Lodash, AngularJS và JSX. Tôi không thích việc tạo mẫu trong AngularJS vì nó sử dụng nhiều chuỗi trong các chỉ thị và lỗi chính tả sẽ không bị bắt. JSX là yêu thích mới của tôi vì nó gần với JavaScript hơn và hầu như không có cú pháp nào để học. Ngày nay, bạn thậm chí có thể sử dụng các chuỗi ký tự mẫu ES2015 như một cách nhanh chóng để tạo mẫu mà không cần dựa vào mã của bên thứ ba.

const template = `<div>My name is: ${name}</div>`;

Tuy nhiên, hãy lưu ý về khả năng XSS tiềm ẩn trong cách tiếp cận trên vì nội dung không được thoát cho bạn, không giống như trong các thư viện tạo mẫu.

Giải thích "hoisting".

Hoisting là một thuật ngữ được sử dụng để giải thích hành vi khai báo biến trong mã của bạn. Các biến được khai báo hoặc khởi tạo bằng từ khóa 'var' sẽ có khai báo của chúng được "di chuyển" lên đầu phạm vi mô-đun/cấp hàm của chúng, mà chúng ta gọi là hoisting. Tuy nhiên, chỉ có khai báo được hoisted, việc gán (nếu có) sẽ ở lại vị trí của nó.

Lưu ý rằng khai báo không thực sự được di chuyển - công cụ JavaScript phân tích các khai báo trong quá trình biên dịch và nhận biết các khai báo và phạm vi của chúng. Việc hình dung các khai báo được hoisted lên đầu phạm vi của chúng chỉ giúp dễ hiểu hơn về hành vi này. Hãy giải thích bằng một vài ví dụ.

console.log(foo); // undefined var foo = 1; console.log(foo); // 1

Các khai báo hàm có thân được hoisted trong khi các biểu thức hàm (được viết dưới dạng khai báo biến) chỉ có khai báo biến được hoisted.

// Khai báo hàm console.log(foo); // [Function: foo] foo(); // 'FOOOOO' function foo() { console.log('FOOOOO'); } console.log(foo); // [Function: foo] // Biểu thức hàm console.log(bar); // undefined bar(); // Uncaught TypeError: bar is not a function var bar = function () { console.log('BARRRR'); }; console.log(bar); // [Function: bar]

Các biến được khai báo thông qua 'let' và 'const' cũng được hoisted. Tuy nhiên, không giống như 'var' và 'function', chúng không được khởi tạo và việc truy cập chúng trước khi khai báo sẽ dẫn đến một ngoại lệ 'ReferenceError'. Biến nằm trong "vùng chết tạm thời" từ đầu khối cho đến khi khai báo được xử lý.

x; // undefined y; // Reference error: y is not defined var x = 'local'; let y = 'local';

Mô tả sự nổi bọt của sự kiện.

Khi một sự kiện kích hoạt trên một phần tử DOM, nó sẽ cố gắng xử lý sự kiện nếu có một trình lắng nghe được gắn vào, sau đó sự kiện sẽ nổi lên phần tử cha của nó và điều tương tự sẽ xảy ra. Sự nổi bọt này xảy ra lên đến các tổ tiên của phần tử cho đến khi đến 'document'. Sự nổi bọt sự kiện là cơ chế đằng sau việc ủy quyền sự kiện.

Sự khác biệt giữa "thuộc tính" và "thuộc tính" là gì?

Các thuộc tính được định nghĩa trên mã HTML nhưng các thuộc tính được định nghĩa trên DOM. Để minh họa sự khác biệt, hãy tưởng tượng chúng ta có trường văn bản này trong HTML của chúng ta: '<input type="text" value="Hello">'.

const input = document.querySelector('input'); console.log(input.getAttribute('value')); // Hello console.log(input.value); // Hello

Nhưng sau khi bạn thay đổi giá trị của trường văn bản bằng cách thêm "World!" vào đó, điều này trở thành:

console.log(input.getAttribute('value')); // Hello console.log(input.value); // Hello World!

Tại sao việc mở rộng các đối tượng JavaScript tích hợp sẵn lại không phải là một ý hay?

Mở rộng một đối tượng JavaScript tích hợp sẵn/native có nghĩa là thêm các thuộc tính/hàm vào 'prototype' của nó. Mặc dù điều này có vẻ là một ý hay ban đầu, nhưng nó nguy hiểm trong thực tế. Hãy tưởng tượng mã của bạn sử dụng một vài thư viện đều mở rộng 'Array.prototype' bằng cách thêm cùng một phương thức 'contains', các triển khai sẽ ghi đè lẫn nhau và mã của bạn sẽ bị lỗi nếu hành vi của hai phương thức này không giống nhau.

Thời điểm duy nhất bạn có thể muốn mở rộng một đối tượng native là khi bạn muốn tạo một polyfill, về cơ bản cung cấp triển khai riêng của bạn cho một phương thức là một phần của đặc tả JavaScript nhưng có thể không tồn tại trong trình duyệt của người dùng do nó là trình duyệt cũ hơn.

Sự khác biệt giữa sự kiện 'load' của tài liệu và sự kiện 'DOMContentLoaded' của tài liệu?

Sự kiện 'DOMContentLoaded' được kích hoạt khi tài liệu HTML ban đầu đã được tải và phân tích cú pháp hoàn toàn, mà không cần chờ bảng kiểu, hình ảnh và khung con tải xong.

Sự kiện 'load' của 'window' chỉ được kích hoạt sau khi DOM và tất cả các tài nguyên và tài sản phụ thuộc đã tải xong.

Sự khác biệt giữa '==' và '===' là gì?

'==' là toán tử so sánh bằng trừu tượng trong khi '===' là toán tử so sánh bằng nghiêm ngặt. Toán tử '==' sẽ so sánh bằng sau khi thực hiện bất kỳ chuyển đổi kiểu cần thiết nào. Toán tử '===' sẽ không thực hiện chuyển đổi kiểu, vì vậy nếu hai giá trị không cùng kiểu thì '===' sẽ chỉ trả về 'false'. Khi sử dụng '==', những điều kỳ lạ có thể xảy ra, chẳng hạn như:

1 == '1'; // true 1 == [1]; // true 1 == true; // true 0 == ''; // true 0 == '0'; // true 0 == false; // true

Lời khuyên của tôi là không bao giờ sử dụng toán tử '==', ngoại trừ để tiện lợi khi so sánh với 'null' hoặc 'undefined', trong đó 'a == null' sẽ trả về 'true' nếu 'a' là 'null' hoặc 'undefined'.

var a = null; console.log(a == null); // true console.log(a == undefined); // true

Giải thích chính sách cùng nguồn gốc liên quan đến JavaScript.

Chính sách cùng nguồn gốc ngăn chặn JavaScript thực hiện các yêu cầu vượt qua ranh giới miền. Một nguồn gốc được định nghĩa là sự kết hợp của lược đồ URI, tên máy chủ và số cổng. Chính sách này ngăn chặn một tập lệnh độc hại trên một trang có được quyền truy cập vào dữ liệu nhạy cảm trên một trang web khác thông qua Mô hình đối tượng tài liệu của trang đó.

Làm cho cái này hoạt động:

duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]
function duplicate(arr) { return arr.concat(arr); } duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

Hoặc với ES6:

const duplicate = (arr) => [...arr, ...arr]; duplicate([1, 2, 3, 4, 5]); // [1,2,3,4,5,1,2,3,4,5]

Tại sao nó được gọi là biểu thức Ternary, từ "Ternary" chỉ ra điều gì?

"Ternary" chỉ ra ba, và một biểu thức ternary chấp nhận ba toán hạng, điều kiện kiểm tra, biểu thức "then" và biểu thức "else". Biểu thức ternary không dành riêng cho JavaScript và tôi không chắc tại sao nó lại nằm trong danh sách này.

''use strict''; là gì? Ưu và nhược điểm của việc sử dụng nó là gì?

'use strict' là một câu lệnh được sử dụng để bật chế độ nghiêm ngặt cho toàn bộ tập lệnh hoặc các hàm riêng lẻ. Chế độ nghiêm ngặt là một cách để chọn tham gia vào một biến thể JavaScript bị hạn chế.

Ưu điểm:

  • Không thể vô tình tạo biến toàn cục.
  • Khiến các phép gán mà nếu không sẽ âm thầm thất bại phải ném ra một ngoại lệ.
  • Khiến các nỗ lực xóa các thuộc tính không thể xóa phải ném ra một ngoại lệ (trong khi trước đây nỗ lực đó sẽ không có tác dụng).
  • Yêu cầu tên tham số hàm phải là duy nhất.
  • 'this' không xác định trong ngữ cảnh toàn cục.
  • Nó bắt một số lỗi mã hóa phổ biến, ném ra các ngoại lệ.
  • Nó tắt các tính năng gây nhầm lẫn hoặc được suy nghĩ kém.

Nhược điểm:

  • Nhiều tính năng bị thiếu mà một số nhà phát triển có thể đã quen thuộc.
  • Không còn quyền truy cập vào 'function.caller' và 'function.arguments'.
  • Việc nối các tập lệnh được viết trong các chế độ nghiêm ngặt khác nhau có thể gây ra vấn đề.

Nhìn chung, tôi nghĩ rằng lợi ích lớn hơn nhược điểm, và tôi chưa bao giờ phải dựa vào các tính năng mà chế độ nghiêm ngặt chặn. Tôi khuyên bạn nên sử dụng chế độ nghiêm ngặt.

Tạo một vòng lặp for lặp đến '100' đồng thời xuất **"fizz"** ở bội số của '3', **"buzz"** ở bội số của '5' và **"fizzbuzz"** ở bội số của '3' và '5'.

Hãy xem phiên bản FizzBuzz này của [Paul Irish].

for (let i = 1; i <= 100; i++) { let f = i % 3 == 0, b = i % 5 == 0; console.log(f ? (b ? 'FizzBuzz' : 'Fizz') : b ? 'Buzz' : i); }

Tuy nhiên, tôi không khuyên bạn nên viết mã trên trong các cuộc phỏng vấn. Chỉ cần tuân thủ cách tiếp cận dài nhưng rõ ràng. Để biết thêm các phiên bản FizzBuzz kỳ quặc, hãy xem liên kết tham khảo bên dưới.

Tại sao, nói chung, việc để nguyên phạm vi toàn cục của một trang web và không bao giờ chạm vào nó lại là một ý hay?

Mọi tập lệnh đều có quyền truy cập vào phạm vi toàn cục, và nếu mọi người đều sử dụng không gian tên toàn cục để định nghĩa các biến của họ, xung đột rất có thể sẽ xảy ra. Sử dụng mẫu mô-đun (IIFE) để đóng gói các biến của bạn trong một không gian tên cục bộ.

Tại sao bạn lại sử dụng một cái gì đó như sự kiện 'load'? Sự kiện này có nhược điểm không? Bạn có biết bất kỳ lựa chọn thay thế nào không và tại sao bạn lại sử dụng chúng?

Sự kiện 'load' kích hoạt vào cuối quá trình tải tài liệu. Tại thời điểm này, tất cả các đối tượng trong tài liệu đều nằm trong DOM, và tất cả hình ảnh, tập lệnh, liên kết và khung con đã tải xong.

Sự kiện DOM 'DOMContentLoaded' sẽ kích hoạt sau khi DOM cho trang đã được xây dựng, nhưng không chờ các tài nguyên khác tải xong. Điều này được ưu tiên trong một số trường hợp khi bạn không cần tải toàn bộ trang trước khi khởi tạo.

Giải thích ứng dụng trang đơn là gì và cách làm cho nó thân thiện với SEO.

Phần dưới đây được lấy từ [Grab Front End Guide] tuyệt vời, tình cờ thay, được viết bởi tôi!

Các nhà phát triển web ngày nay gọi các sản phẩm họ xây dựng là ứng dụng web, thay vì trang web. Mặc dù không có sự khác biệt rõ ràng giữa hai thuật ngữ này, ứng dụng web có xu hướng tương tác cao và động, cho phép người dùng thực hiện các hành động và nhận phản hồi cho hành động của họ. Theo truyền thống, trình duyệt nhận HTML từ máy chủ và hiển thị nó. Khi người dùng điều hướng đến một URL khác, cần phải làm mới toàn bộ trang và máy chủ gửi HTML mới cho trang mới. Đây được gọi là kết xuất phía máy chủ.

Tuy nhiên, trong các SPA hiện đại, kết xuất phía máy khách được sử dụng thay thế. Trình duyệt tải trang ban đầu từ máy chủ, cùng với các tập lệnh (khung, thư viện, mã ứng dụng) và biểu định kiểu cần thiết cho toàn bộ ứng dụng. Khi người dùng điều hướng đến các trang khác, việc làm mới trang không được kích hoạt. URL của trang được cập nhật thông qua [HTML5 History API]. Dữ liệu mới cần thiết cho trang mới, thường ở định dạng JSON, được trình duyệt truy xuất thông qua các yêu cầu [AJAX] đến máy chủ. SPA sau đó tự động cập nhật trang bằng dữ liệu thông qua JavaScript, mà nó đã tải xuống trong lần tải trang ban đầu. Mô hình này tương tự như cách các ứng dụng di động gốc hoạt động.

Ưu điểm:

  • Ứng dụng phản hồi nhanh hơn và người dùng không thấy sự nhấp nháy giữa các lần điều hướng trang do làm mới toàn bộ trang.
  • Ít yêu cầu HTTP được gửi đến máy chủ hơn, vì cùng một tài sản không phải tải xuống lại cho mỗi lần tải trang.
  • Tách biệt rõ ràng các mối quan tâm giữa máy khách và máy chủ; bạn có thể dễ dàng xây dựng các máy khách mới cho các nền tảng khác nhau (ví dụ: di động, chatbot, đồng hồ thông minh) mà không cần phải sửa đổi mã máy chủ. Bạn cũng có thể sửa đổi ngăn xếp công nghệ trên máy khách và máy chủ một cách độc lập, miễn là hợp đồng API không bị phá vỡ.

Nhược điểm:

  • Tải trang ban đầu nặng hơn do tải khung, mã ứng dụng và tài sản cần thiết cho nhiều trang.
  • Có một bước bổ sung cần thực hiện trên máy chủ của bạn là cấu hình nó để định tuyến tất cả các yêu cầu đến một điểm vào duy nhất và cho phép định tuyến phía máy khách tiếp quản từ đó.
  • SPA phụ thuộc vào JavaScript để hiển thị nội dung, nhưng không phải tất cả các công cụ tìm kiếm đều thực thi JavaScript trong quá trình thu thập thông tin và chúng có thể thấy nội dung trống trên trang của bạn. Điều này vô tình làm tổn hại đến Tối ưu hóa Công cụ Tìm kiếm (SEO) của ứng dụng của bạn. Tuy nhiên, hầu hết thời gian, khi bạn đang xây dựng ứng dụng, SEO không phải là yếu tố quan trọng nhất, vì không phải tất cả nội dung đều cần được các công cụ tìm kiếm lập chỉ mục. Để khắc phục điều này, bạn có thể kết xuất phía máy chủ ứng dụng của mình hoặc sử dụng các dịch vụ như [Prerender] để kết xuất JavaScript của bạn trong trình duyệt, lưu HTML tĩnh và trả về cho các trình thu thập thông tin.

Kinh nghiệm của bạn với Promises và/hoặc các polyfills của chúng đến mức nào?

Có kiến thức thực tế về nó. Một promise là một đối tượng có thể tạo ra một giá trị duy nhất vào một thời điểm nào đó trong tương lai: hoặc một giá trị được giải quyết hoặc một lý do mà nó không được giải quyết (ví dụ: xảy ra lỗi mạng). Một promise có thể ở một trong 3 trạng thái có thể: đã hoàn thành, bị từ chối hoặc đang chờ xử lý. Người dùng promise có thể đính kèm các hàm callback để xử lý giá trị đã hoàn thành hoặc lý do bị từ chối.

Một số polyfill phổ biến là $.deferred, Q và Bluebird nhưng không phải tất cả chúng đều tuân thủ đặc tả. ES2015 hỗ trợ Promises ngay lập tức và các polyfill thường không cần thiết vào thời điểm hiện tại.

Ưu và nhược điểm của việc sử dụng Promises thay vì callbacks là gì?

Ưu điểm

  • Tránh callback hell có thể gây khó đọc.
  • Giúp dễ dàng viết mã bất đồng bộ tuần tự, dễ đọc với .then().
  • Giúp dễ dàng viết mã bất đồng bộ song song với Promise.all().
  • Với promises, những kịch bản sau đây có thể xảy ra trong mã hóa chỉ sử dụng callbacks sẽ không xảy ra:
    • Gọi callback quá sớm
    • Gọi callback quá muộn (hoặc không bao giờ)
    • Gọi callback quá ít hoặc quá nhiều lần
    • Không truyền bất kỳ môi trường/tham số cần thiết nào
    • Nuốt bất kỳ lỗi/ngoại lệ nào có thể xảy ra

Nhược điểm

  • Mã phức tạp hơn một chút (có thể tranh cãi).
  • Trong các trình duyệt cũ hơn không hỗ trợ ES2015, bạn cần tải polyfill để sử dụng nó.

Một số ưu điểm/nhược điểm của việc viết mã JavaScript bằng ngôn ngữ biên dịch sang JavaScript là gì?

Một số ví dụ về ngôn ngữ biên dịch sang JavaScript bao gồm CoffeeScript, Elm, ClojureScript, PureScript và TypeScript.

Ưu điểm:

  • Khắc phục một số vấn đề tồn tại lâu dài trong JavaScript và không khuyến khích các anti-pattern của JavaScript.
  • Cho phép bạn viết mã ngắn hơn, bằng cách cung cấp một số syntactic sugar trên JavaScript, mà tôi nghĩ ES5 thiếu, nhưng ES2015 thì tuyệt vời.
  • Kiểu tĩnh rất tuyệt vời (trong trường hợp của TypeScript) cho các dự án lớn cần được duy trì theo thời gian.

Nhược điểm:

  • Yêu cầu quá trình xây dựng/biên dịch vì trình duyệt chỉ chạy JavaScript và mã của bạn sẽ cần được biên dịch thành JavaScript trước khi được phục vụ cho trình duyệt.
  • Gỡ lỗi có thể khó khăn nếu source map của bạn không ánh xạ tốt đến mã nguồn được biên dịch trước của bạn.
  • Hầu hết các nhà phát triển không quen thuộc với các ngôn ngữ này và sẽ cần học nó. Có một chi phí học hỏi liên quan cho nhóm của bạn nếu bạn sử dụng nó cho các dự án của mình.
  • Cộng đồng nhỏ hơn (tùy thuộc vào ngôn ngữ), có nghĩa là tài nguyên, hướng dẫn, thư viện và công cụ sẽ khó tìm hơn.
  • Hỗ trợ IDE/trình chỉnh sửa có thể bị thiếu.
  • Các ngôn ngữ này sẽ luôn đi sau tiêu chuẩn JavaScript mới nhất.
  • Các nhà phát triển nên nhận thức được mã của họ đang được biên dịch thành gì — bởi vì đó là những gì thực sự sẽ chạy, và đó là điều quan trọng cuối cùng.

Trong thực tế, ES2015 đã cải thiện đáng kể JavaScript và làm cho nó dễ viết hơn nhiều. Tôi thực sự không thấy cần CoffeeScript vào thời điểm hiện tại.

Bạn sử dụng công cụ và kỹ thuật nào để gỡ lỗi mã JavaScript?

  • React và Redux
    • React Devtools
    • Redux Devtools
  • Vue
    • Vue Devtools
  • JavaScript
    • Chrome Devtools
    • câu lệnh debugger
    • Gỡ lỗi console.log cũ tốt

Bạn sử dụng cấu trúc ngôn ngữ nào để lặp qua các thuộc tính đối tượng và các mục mảng?

Đối với các đối tượng:

  • vòng lặp for-in - for (var property in obj) { console.log(property); }. Tuy nhiên, điều này cũng sẽ lặp qua các thuộc tính kế thừa của nó và bạn sẽ thêm kiểm tra obj.hasOwnProperty(property) trước khi sử dụng nó.
  • Object.keys() - Object.keys(obj).forEach(function (property) { ... }). Object.keys() là một phương thức tĩnh sẽ liệt kê tất cả các thuộc tính có thể liệt kê của đối tượng mà bạn truyền cho nó.
  • Object.getOwnPropertyNames() - Object.getOwnPropertyNames(obj).forEach(function (property) { ... }). Object.getOwnPropertyNames() là một phương thức tĩnh sẽ liệt kê tất cả các thuộc tính có thể liệt kê và không thể liệt kê của đối tượng mà bạn truyền cho nó.

Đối với các mảng:

  • vòng lặp for - for (var i = 0; i < arr.length; i++). Lỗi phổ biến ở đây là var nằm trong phạm vi hàm chứ không phải phạm vi khối và hầu hết thời gian bạn muốn biến lặp có phạm vi khối. ES2015 giới thiệu let có phạm vi khối và khuyến nghị sử dụng nó thay thế. Vì vậy, điều này trở thành: for (let i = 0; i < arr.length; i++).
  • forEach - arr.forEach(function (el, index) { ... }). Cấu trúc này đôi khi có thể thuận tiện hơn vì bạn không phải sử dụng index nếu tất cả những gì bạn cần là các phần tử mảng. Ngoài ra còn có các phương thức every và some sẽ cho phép bạn chấm dứt việc lặp sớm.
  • vòng lặp for-of - for (let elem of arr) { ... }. ES6 giới thiệu một vòng lặp mới, vòng lặp for-of, cho phép bạn lặp qua các đối tượng tuân thủ giao thức lặp như String, Array, Map, Set, v.v. Nó kết hợp các ưu điểm của vòng lặp for và phương thức forEach(). Ưu điểm của vòng lặp for là bạn có thể thoát khỏi nó, và ưu điểm của forEach() là nó ngắn gọn hơn vòng lặp for vì bạn không cần biến đếm. Với vòng lặp for-of, bạn có cả khả năng thoát khỏi vòng lặp và cú pháp ngắn gọn hơn.

Hầu hết thời gian, tôi sẽ ưu tiên phương thức .forEach, nhưng điều đó thực sự phụ thuộc vào những gì bạn đang cố gắng làm. Trước ES6, chúng tôi đã sử dụng vòng lặp for khi chúng tôi cần kết thúc vòng lặp sớm bằng cách sử dụng break. Nhưng bây giờ với ES6, chúng tôi có thể làm điều đó với vòng lặp for-of. Tôi sẽ sử dụng vòng lặp for khi tôi cần linh hoạt hơn nữa, chẳng hạn như tăng biến lặp nhiều hơn một lần mỗi vòng lặp.

Ngoài ra, khi sử dụng vòng lặp for-of, nếu bạn cần truy cập cả chỉ mục và giá trị của từng phần tử mảng, bạn có thể làm như vậy với phương thức entries() của Array ES6 và destructuring:

const arr = ['a', 'b', 'c']; for (let [index, elem] of arr.entries()) { console.log(index, ': ', elem); }

Giải thích sự khác biệt giữa các đối tượng có thể thay đổi (mutable) và bất biến (immutable).

Tính bất biến là một nguyên tắc cốt lõi trong lập trình hàm và cũng có nhiều điều để cung cấp cho các chương trình hướng đối tượng. Một đối tượng có thể thay đổi là một đối tượng mà trạng thái của nó có thể được sửa đổi sau khi nó được tạo. Một đối tượng bất biến là một đối tượng mà trạng thái của nó không thể được sửa đổi sau khi nó được tạo.

Ví dụ về đối tượng bất biến trong JavaScript?

Trong JavaScript, một số kiểu dựng sẵn (số, chuỗi) là bất biến, nhưng các đối tượng tùy chỉnh thường có thể thay đổi.

Một số đối tượng JavaScript bất biến được dựng sẵn là Math, Date.

Dưới đây là một vài cách để thêm/mô phỏng tính bất biến trên các đối tượng JavaScript thuần túy.

Thuộc tính hằng của đối tượng

Bằng cách kết hợp writable: false và configurable: false, bạn có thể tạo một hằng số (không thể thay đổi, định nghĩa lại hoặc xóa) làm thuộc tính đối tượng, như:

let myObject = {}; Object.defineProperty(myObject, 'number', { value: 42, writable: false, configurable: false, }); console.log(myObject.number); // 42 myObject.number = 43; console.log(myObject.number); // 42

Ngăn chặn mở rộng

Nếu bạn muốn ngăn một đối tượng thêm các thuộc tính mới vào nó, nhưng vẫn giữ nguyên các thuộc tính khác của đối tượng, hãy gọi Object.preventExtensions(...):

var myObject = { a: 2, }; Object.preventExtensions(myObject); myObject.b = 3; myObject.b; // undefined

Ở chế độ không nghiêm ngặt, việc tạo b thất bại một cách âm thầm. Ở chế độ nghiêm ngặt, nó ném ra một TypeError.

Seal

Object.seal() tạo một đối tượng đã niêm phong, có nghĩa là nó lấy một đối tượng hiện có và về cơ bản gọi Object.preventExtensions() trên nó, nhưng cũng đánh dấu tất cả các thuộc tính hiện có của nó là configurable: false.

Vì vậy, bạn không chỉ không thể thêm bất kỳ thuộc tính nào nữa mà bạn cũng không thể định cấu hình lại hoặc xóa bất kỳ thuộc tính hiện có nào (mặc dù bạn vẫn có thể sửa đổi giá trị của chúng).

Freeze

Object.freeze() tạo một đối tượng đóng băng, có nghĩa là nó lấy một đối tượng hiện có và về cơ bản gọi Object.seal() trên nó, nhưng nó cũng đánh dấu tất cả các thuộc tính trình truy cập dữ liệu là writable:false, để giá trị của chúng không thể bị thay đổi.

Cách tiếp cận này là mức độ bất biến cao nhất mà bạn có thể đạt được cho chính một đối tượng, vì nó ngăn chặn mọi thay đổi đối với đối tượng hoặc bất kỳ thuộc tính trực tiếp nào của nó (mặc dù, như đã đề cập ở trên, nội dung của bất kỳ đối tượng nào được tham chiếu khác đều không bị ảnh hưởng).

var immutable = Object.freeze({});

Đóng băng một đối tượng không cho phép thêm các thuộc tính mới vào một đối tượng và ngăn chặn việc xóa hoặc thay đổi các thuộc tính hiện có. Object.freeze() bảo toàn tính liệt kê, khả năng cấu hình, khả năng ghi và nguyên mẫu của đối tượng. Nó trả về đối tượng đã truyền và không tạo một bản sao đóng băng.

Ưu và nhược điểm của tính bất biến là gì?

Ưu điểm

  • Phát hiện thay đổi dễ dàng hơn - Sự bằng nhau của đối tượng có thể được xác định một cách hiệu quả và dễ dàng thông qua sự bằng nhau tham chiếu. Điều này hữu ích để so sánh sự khác biệt của đối tượng trong React và Redux.
  • Các chương trình với các đối tượng bất biến ít phức tạp hơn để suy nghĩ, vì bạn không cần phải lo lắng về cách một đối tượng có thể phát triển theo thời gian.
  • Các bản sao phòng thủ không còn cần thiết khi các đối tượng bất biến được trả về từ hoặc truyền vào các hàm, vì không có khả năng một đối tượng bất biến sẽ bị sửa đổi bởi nó.
  • Chia sẻ dễ dàng thông qua tham chiếu - Một bản sao của một đối tượng cũng tốt như một bản sao khác, vì vậy bạn có thể lưu trữ các đối tượng hoặc sử dụng lại cùng một đối tượng nhiều lần.
  • An toàn luồng - Các đối tượng bất biến có thể được sử dụng an toàn giữa các luồng trong môi trường đa luồng vì không có rủi ro chúng bị sửa đổi trong các luồng khác đang chạy đồng thời.
  • Sử dụng các thư viện như ImmutableJS, các đối tượng được sửa đổi bằng cách chia sẻ cấu trúc và cần ít bộ nhớ hơn để có nhiều đối tượng có cấu trúc tương tự.

Nhược điểm

  • Các triển khai ngây thơ của cấu trúc dữ liệu bất biến và các hoạt động của nó có thể dẫn đến hiệu suất cực kỳ kém vì các đối tượng mới được tạo mỗi lần. Khuyến nghị sử dụng thư viện cho cấu trúc dữ liệu bất biến hiệu quả và các hoạt động tận dụng chia sẻ cấu trúc.
  • Phân bổ (và giải phóng) nhiều đối tượng nhỏ thay vì sửa đổi các đối tượng hiện có có thể gây ảnh hưởng đến hiệu suất. Độ phức tạp của bộ cấp phát hoặc bộ thu gom rác thường phụ thuộc vào số lượng đối tượng trên heap.
  • Các cấu trúc dữ liệu tuần hoàn như đồ thị rất khó xây dựng. Nếu bạn có hai đối tượng không thể sửa đổi sau khi khởi tạo, làm thế nào bạn có thể khiến chúng trỏ vào nhau?

Làm thế nào bạn có thể đạt được tính bất biến trong mã của riêng bạn?

Cách khác là sử dụng khai báo const kết hợp với các kỹ thuật đã đề cập ở trên để tạo. Đối với các đối tượng có thể thay đổi, hãy sử dụng toán tử spread, Object.assign, Array.concat(), v.v., để tạo các đối tượng mới thay vì thay đổi đối tượng gốc.

Ví dụ:

// Ví dụ Mảng const arr = [1, 2, 3]; const newArr = [...arr, 4]; // [1, 2, 3, 4] // Ví dụ Đối tượng const human = Object.freeze({ race: 'human' }); const john = { ...human, name: 'John' }; // {race: "human", name: "John"} const alienJohn = { ...john, race: 'alien' }; // {race: "alien", name: "John"}

Giải thích sự khác biệt giữa các hàm đồng bộ và bất đồng bộ.

Các hàm đồng bộ là chặn trong khi các hàm bất đồng bộ thì không. Trong các hàm đồng bộ, các câu lệnh hoàn thành trước khi câu lệnh tiếp theo được chạy. Trong trường hợp này, chương trình được đánh giá chính xác theo thứ tự của các câu lệnh và việc thực thi chương trình bị tạm dừng nếu một trong các câu lệnh mất rất nhiều thời gian.

Các hàm bất đồng bộ thường chấp nhận một hàm callback làm tham số và việc thực thi tiếp tục trên dòng tiếp theo ngay sau khi hàm bất đồng bộ được gọi. Hàm callback chỉ được gọi khi thao tác bất đồng bộ hoàn tất và ngăn xếp cuộc gọi trống. Các thao tác nặng như tải dữ liệu từ máy chủ web hoặc truy vấn cơ sở dữ liệu nên được thực hiện bất đồng bộ để luồng chính có thể tiếp tục thực thi các thao tác khác thay vì chặn cho đến khi thao tác dài đó hoàn tất (trong trường hợp trình duyệt, giao diện người dùng sẽ bị đóng băng).

Vòng lặp sự kiện là gì? Sự khác biệt giữa ngăn xếp cuộc gọi và hàng đợi tác vụ là gì?

Vòng lặp sự kiện là một vòng lặp đơn luồng giám sát ngăn xếp cuộc gọi và kiểm tra xem có công việc nào cần thực hiện trong hàng đợi tác vụ không. Nếu ngăn xếp cuộc gọi trống và có các hàm callback trong hàng đợi tác vụ, một hàm sẽ được đưa ra khỏi hàng đợi và đẩy vào ngăn xếp cuộc gọi để thực thi.

Nếu bạn chưa xem bài nói chuyện của Philip Robert về Vòng lặp sự kiện, bạn nên xem. Đây là một trong những video được xem nhiều nhất về JavaScript.

Giải thích sự khác biệt trong việc sử dụng foo giữa function foo() {} và var foo = function() {}

Trường hợp đầu tiên là khai báo hàm trong khi trường hợp sau là biểu thức hàm. Sự khác biệt chính là khai báo hàm có phần thân được hoist nhưng phần thân của biểu thức hàm thì không (chúng có hành vi hoisting tương tự như các biến). Để biết thêm giải thích về hoisting, hãy tham khảo câu hỏi trên về hoisting. Nếu bạn cố gắng gọi một biểu thức hàm trước khi nó được định nghĩa, bạn sẽ nhận được lỗi Uncaught TypeError: XXX is not a function.

Khai báo hàm

foo(); // 'FOOOOO' function foo() { console.log('FOOOOO'); }

Biểu thức hàm

foo(); // Uncaught TypeError: foo is not a function var foo = function () { console.log('FOOOOO'); };

Sự khác biệt giữa các biến được tạo bằng let, var hoặc const là gì?

Các biến được khai báo bằng từ khóa var có phạm vi là hàm mà chúng được tạo ra, hoặc nếu được tạo bên ngoài bất kỳ hàm nào, là đối tượng toàn cục. let và const có phạm vi khối, nghĩa là chúng chỉ có thể truy cập được trong tập hợp dấu ngoặc nhọn gần nhất (hàm, khối if-else hoặc vòng lặp for).

function foo() { // Tất cả các biến đều có thể truy cập được trong các hàm. var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; console.log(bar); // bar console.log(baz); // baz console.log(qux); // qux } console.log(bar); // ReferenceError: bar is not defined console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined
if (true) { var bar = 'bar'; let baz = 'baz'; const qux = 'qux'; } // Các biến được khai báo bằng var có thể truy cập được ở bất kỳ đâu trong phạm vi hàm. console.log(bar); // bar // Các biến được định nghĩa bằng let và const không thể truy cập được bên ngoài khối mà chúng được định nghĩa. console.log(baz); // ReferenceError: baz is not defined console.log(qux); // ReferenceError: qux is not defined

var cho phép các biến được hoisted, nghĩa là chúng có thể được tham chiếu trong mã trước khi chúng được khai báo. let và const sẽ không cho phép điều này, thay vào đó sẽ ném lỗi.

console.log(foo); // undefined var foo = 'foo'; console.log(baz); // ReferenceError: can't access lexical declaration 'baz' before initialization let baz = 'baz'; console.log(bar); // ReferenceError: can't access lexical declaration 'bar' before initialization const bar = 'bar';

Khai báo lại một biến với var sẽ không ném lỗi, nhưng let và const sẽ.

var foo = 'foo'; var foo = 'bar'; console.log(foo); // "bar" let baz = 'baz'; let baz = 'qux'; // Uncaught SyntaxError: Identifier 'baz' has already been declared

let và const khác nhau ở chỗ let cho phép gán lại giá trị của biến trong khi const thì không.

// Điều này ổn. let foo = 'foo'; foo = 'bar'; // Điều này gây ra một ngoại lệ. const baz = 'baz'; baz = 'qux';

Sự khác biệt giữa lớp ES6 và hàm khởi tạo ES5 là gì?

Hãy cùng xem ví dụ về từng loại:

// Hàm tạo ES5 function Person(name) { this.name = name; } // Lớp ES6 class Person { constructor(name) { this.name = name; } }

Đối với các hàm tạo đơn giản, chúng trông khá giống nhau.

Sự khác biệt chính trong hàm tạo xuất hiện khi sử dụng kế thừa. Nếu chúng ta muốn tạo một lớp Student kế thừa từ Person và thêm trường studentId, đây là những gì chúng ta phải làm ngoài những điều trên.

// Hàm tạo ES5 function Student(name, studentId) { // Gọi hàm tạo của lớp cha để khởi tạo các thành viên được kế thừa từ lớp cha. Person.call(this, name); // Khởi tạo các thành viên riêng của lớp con. this.studentId = studentId; } Student.prototype = Object.create(Person.prototype); Student.prototype.constructor = Student; // Lớp ES6 class Student extends Person { constructor(name, studentId) { super(name); this.studentId = studentId; } }

Việc sử dụng kế thừa trong ES5 phức tạp hơn nhiều và phiên bản ES6 dễ hiểu và dễ nhớ hơn.

Bạn có thể đưa ra một trường hợp sử dụng cho cú pháp hàm mũi tên => mới không? Cú pháp mới này khác với các hàm khác như thế nào?

Một lợi ích rõ ràng của các hàm mũi tên là đơn giản hóa cú pháp cần thiết để tạo hàm, không cần từ khóa function. Từ khóa this trong các hàm mũi tên cũng được liên kết với phạm vi bao quanh, khác với các hàm thông thường, nơi this được xác định bởi đối tượng gọi nó. this được xác định theo ngữ cảnh rất hữu ích khi gọi lại các hàm callback, đặc biệt là trong các thành phần React.

Ưu điểm của việc sử dụng cú pháp mũi tên cho một phương thức trong một hàm tạo là gì?

Ưu điểm chính của việc sử dụng hàm mũi tên làm phương thức bên trong hàm tạo là giá trị của this được đặt tại thời điểm tạo hàm và không thể thay đổi sau đó. Vì vậy, khi hàm tạo được sử dụng để tạo một đối tượng mới, this sẽ luôn tham chiếu đến đối tượng đó. Ví dụ, giả sử chúng ta có một hàm tạo Person nhận tên đầu tiên làm đối số có hai phương thức để console.log tên đó, một là hàm thông thường và một là hàm mũi tên:

const Person = function (firstName) { this.firstName = firstName; this.sayName1 = function () { console.log(this.firstName); }; this.sayName2 = () => { console.log(this.firstName); }; }; const john = new Person('John'); const dave = new Person('Dave'); john.sayName1(); // John john.sayName2(); // John // Hàm thông thường có thể thay đổi giá trị 'this' của nó, nhưng hàm mũi tên thì không thể john.sayName1.call(dave); // Dave (vì "this" bây giờ là đối tượng dave) john.sayName2.call(dave); // John john.sayName1.apply(dave); // Dave (vì 'this' bây giờ là đối tượng dave) john.sayName2.apply(dave); // John john.sayName1.bind(dave)(); // Dave (vì 'this' bây giờ là đối tượng dave) john.sayName2.bind(dave)(); // John var sayNameFromWindow1 = john.sayName1; sayNameFromWindow1(); // undefined (vì 'this' bây giờ là đối tượng window) var sayNameFromWindow2 = john.sayName2; sayNameFromWindow2(); // John

Điểm chính ở đây là this có thể thay đổi đối với một hàm thông thường, nhưng ngữ cảnh luôn giữ nguyên đối với một hàm mũi tên. Vì vậy, ngay cả khi bạn truyền hàm mũi tên của mình đến các phần khác nhau của ứng dụng, bạn sẽ không phải lo lắng về việc ngữ cảnh thay đổi.

Điều này đặc biệt hữu ích trong các thành phần lớp React. Nếu bạn định nghĩa một phương thức lớp cho một thứ gì đó như trình xử lý nhấp chuột bằng cách sử dụng một hàm thông thường, và sau đó bạn truyền trình xử lý nhấp chuột đó xuống một thành phần con dưới dạng một prop, bạn sẽ cần phải liên kết this trong hàm tạo của thành phần cha. Nếu bạn thay vào đó sử dụng một hàm mũi tên, không cần phải liên kết this, vì phương thức sẽ tự động nhận giá trị this của nó từ ngữ cảnh từ vựng bao quanh của nó.

Định nghĩa của hàm bậc cao là gì?

Hàm bậc cao là bất kỳ hàm nào nhận một hoặc nhiều hàm làm đối số, mà nó sử dụng để thao tác trên một số dữ liệu, và/hoặc trả về một hàm làm kết quả. Các hàm bậc cao được thiết kế để trừu tượng hóa một số thao tác được thực hiện lặp đi lặp lại. Ví dụ kinh điển về điều này là map, nhận một mảng và một hàm làm đối số. map sau đó sử dụng hàm này để biến đổi từng mục trong mảng, trả về một mảng mới với dữ liệu đã biến đổi. Các ví dụ phổ biến khác trong JavaScript là forEach, filter và reduce. Một hàm bậc cao không chỉ cần thao tác với mảng vì có nhiều trường hợp sử dụng để trả về một hàm từ một hàm khác. Function.prototype.bind là một ví dụ như vậy trong JavaScript.

Map

Giả sử chúng ta có một mảng các tên mà chúng ta cần biến đổi mỗi chuỗi thành chữ hoa.

const names = ['irish', 'daisy', 'anna'];

Cách thức mệnh lệnh sẽ như sau:

const transformNamesToUppercase = function (names) { const results = []; for (let i = 0; i < names.length; i++) { results.push(names[i].toUpperCase()); } return results; }; transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Sử dụng .map(transformerFn) làm cho mã ngắn gọn và rõ ràng hơn.

const transformNamesToUppercase = function (names) { return names.map((name) => name.toUpperCase()); }; transformNamesToUppercase(names); // ['IRISH', 'DAISY', 'ANNA']

Bạn có thể đưa ra ví dụ về việc hủy cấu trúc một đối tượng hoặc một mảng không?

Hủy cấu trúc là một biểu thức có sẵn trong ES6 cho phép một cách ngắn gọn và thuận tiện để trích xuất các giá trị của Đối tượng hoặc Mảng và đặt chúng vào các biến riêng biệt.

Hủy cấu trúc mảng

// Gán biến. const foo = ['one', 'two', 'three']; const [one, two, three] = foo; console.log(one); // "one" console.log(two); // "two" console.log(three); // "three"
// Hoán đổi biến let a = 1; let b = 3; [a, b] = [b, a]; console.log(a); // 3 console.log(b); // 1

Hủy cấu trúc đối tượng

// Gán biến. const o = { p: 42, q: true }; const { p, q } = o; console.log(p); // 42 console.log(q); // true

ES6 Template Literals cung cấp rất nhiều linh hoạt trong việc tạo chuỗi, bạn có thể cho một ví dụ không?

Template literals giúp đơn giản hóa việc nội suy chuỗi, hoặc bao gồm các biến trong một chuỗi. Trước ES2015, người ta thường làm điều gì đó như thế này:

var person = { name: 'Tyler', age: 28 }; console.log( 'Hi, my name is ' + person.name + ' and I am ' + person.age + ' years old!', ); // 'Hi, my name is Tyler and I am 28 years old!'

Với template literals, bạn có thể tạo cùng một đầu ra như thế này:

const person = { name: 'Tyler', age: 28 }; console.log(`Hi, my name is ${person.name} and I am ${person.age} years old!`); // 'Hi, my name is Tyler and I am 28 years old!'

Lưu ý rằng bạn sử dụng dấu huyền, không phải dấu nháy kép, để chỉ ra rằng bạn đang sử dụng template literal và bạn có thể chèn các biểu thức bên trong các placeholder ${}.

Một trường hợp sử dụng hữu ích thứ hai là tạo chuỗi nhiều dòng. Trước ES2015, bạn có thể tạo chuỗi nhiều dòng như thế này:

console.log('This is line one.\nThis is line two.'); // This is line one. // This is line two.

Hoặc nếu bạn muốn chia nó thành nhiều dòng trong mã của mình để không phải cuộn sang phải trong trình chỉnh sửa văn bản để đọc một chuỗi dài, bạn cũng có thể viết nó như thế này:

console.log('This is line one.\n' + 'This is line two.'); // This is line one. // This is line two.

Tuy nhiên, template literals bảo toàn mọi khoảng trắng bạn thêm vào chúng. Ví dụ, để tạo cùng một đầu ra nhiều dòng mà chúng ta đã tạo ở trên, bạn chỉ cần làm:

console.log(`This is line one. This is line two.`); // This is line one. // This is line two.

Một trường hợp sử dụng khác của template literals là thay thế cho các thư viện templating để nội suy biến đơn giản:

const person = { name: 'Tyler', age: 28 }; document.body.innerHTML = ` <div> <p>Name: ${person.name}</p> <p>Age: ${person.age}</p> </div> `;

Lưu ý rằng mã của bạn có thể dễ bị XSS bằng cách sử dụng .innerHTML. Hãy làm sạch dữ liệu của bạn trước khi hiển thị nếu nó đến từ người dùng!

Bạn có thể đưa ra ví dụ về hàm curry và tại sao cú pháp này lại có ưu điểm?

Currying là một mẫu trong đó một hàm có nhiều hơn một tham số được chia thành nhiều hàm, khi được gọi liên tiếp, sẽ tích lũy tất cả các tham số cần thiết từng cái một. Kỹ thuật này có thể hữu ích để làm cho mã được viết theo kiểu hàm dễ đọc và dễ kết hợp hơn. Điều quan trọng cần lưu ý là để một hàm được curried, nó cần bắt đầu từ một hàm, sau đó được chia thành một chuỗi các hàm mà mỗi hàm chấp nhận một tham số.

function curry(fn) { if (fn.length === 0) { return fn; } function _curried(depth, args) { return function (newArgument) { if (depth - 1 === 0) { return fn(...args, newArgument); } return _curried(depth - 1, [...args, newArgument]); }; } return _curried(fn.length, []); } function add(a, b) { return a + b; } var curriedAdd = curry(add); var addFive = curriedAdd(5); var result = [0, 1, 2, 3, 4, 5].map(addFive); // [5, 6, 7, 8, 9, 10]

Lợi ích của việc sử dụng cú pháp spread là gì và nó khác với cú pháp rest như thế nào?

Cú pháp spread của ES6 rất hữu ích khi viết mã theo mô hình chức năng vì chúng ta có thể dễ dàng tạo bản sao của các mảng hoặc đối tượng mà không cần dùng đến Object.create, slice hoặc một hàm thư viện. Tính năng ngôn ngữ này thường được sử dụng trong các dự án Redux và RxJS.

function putDookieInAnyArray(arr) { return [...arr, 'dookie']; } const result = putDookieInAnyArray(['I', 'really', "don't", 'like']); // ["I", "really", "don't", "like", "dookie"] const person = { name: 'Todd', age: 29, }; const copyOfTodd = { ...person };

Cú pháp rest của ES6 cung cấp một cách viết tắt để bao gồm một số lượng đối số tùy ý được truyền cho một hàm. Nó giống như một nghịch đảo của cú pháp spread, lấy dữ liệu và nhồi nhét nó vào một mảng thay vì giải nén một mảng dữ liệu, và nó hoạt động trong các đối số hàm, cũng như trong các phép gán hủy cấu trúc mảng và đối tượng.

function addFiveToABunchOfNumbers(...numbers) { return numbers.map((x) => x + 5); } const result = addFiveToABunchOfNumbers(4, 5, 6, 7, 8, 9, 10); // [9, 10, 11, 12, 13, 14, 15] const [a, b, ...rest] = [1, 2, 3, 4]; // a: 1, b: 2, rest: [3, 4] const { e, f, ...others } = { e: 1, f: 2, g: 3, h: 4, }; // e: 1, f: 2, others: { g: 3, h: 4 }

Bạn có thể chia sẻ mã giữa các tệp bằng cách nào?

Điều này phụ thuộc vào môi trường JavaScript.

Trên client (môi trường trình duyệt), miễn là các biến/hàm được khai báo trong phạm vi toàn cục (window), tất cả các script có thể tham chiếu đến chúng. Ngoài ra, hãy áp dụng Asynchronous Module Definition (AMD) thông qua RequireJS để có một phương pháp mô-đun hơn.

Trên server (Node.js), cách phổ biến là sử dụng CommonJS. Mỗi tệp được coi là một mô-đun và nó có thể xuất các biến và hàm bằng cách gắn chúng vào đối tượng module.exports.

ES2015 định nghĩa một cú pháp mô-đun nhằm thay thế cả AMD và CommonJS. Điều này cuối cùng sẽ được hỗ trợ trong cả môi trường trình duyệt và Node.

Tại sao bạn muốn tạo các thành viên lớp tĩnh?

Các thành viên lớp tĩnh (thuộc tính/phương thức) không gắn liền với một thể hiện cụ thể của một lớp và có cùng giá trị bất kể thể hiện nào đang tham chiếu đến nó. Các thuộc tính tĩnh thường là các biến cấu hình và các phương thức tĩnh thường là các hàm tiện ích thuần túy không phụ thuộc vào trạng thái của thể hiện.