Bạn có thể đã bắt gặp hoặc viết mã tương tự như thế này khi viết JavaScript:
function sayWord(word) {
return () => console.log(word);
}
const sayHello = sayWord("hello");
sayHello(); // "hello"
Mã này thú vị vì một vài lý do. Đầu tiên, chúng ta có thể truy cập word
trong hàm được trả về từ sayWord
. Thứ hai, chúng tôi có quyền truy cập vào word
giá trị của khi chúng ta gọi sayHello
– mặc dù chúng tôi gọi sayHello
nơi chúng tôi không có quyền truy cập vào word
.
Trong bài viết này, chúng ta sẽ tìm hiểu về phạm vi và bao đóng cho phép hành vi này.
Giới thiệu Phạm vi trong JavaScript
Phạm vi là phần đầu tiên sẽ giúp chúng ta hiểu ví dụ trước. Phạm vi của một biến là một phần của chương trình mà nó có sẵn để sử dụng.
Biến JavaScript có phạm vi từ vựng, nghĩa là chúng ta có thể xác định phạm vi của biến từ nơi nó được khai báo trong mã nguồn. (Điều này không hoàn toàn đúng: var
các biến không có phạm vi từ vựng, nhưng chúng ta sẽ thảo luận về điều đó ngay sau đây.)
Lấy ví dụ sau:
if (true) {
const foo = "foo";
console.log(foo); // "foo"
}
Các if
câu lệnh giới thiệu phạm vi khối bằng cách sử dụng câu lệnh khối. chúng tôi nói rằng foo
là phạm vi khối đến if
bản tường trình. Điều này có nghĩa là nó chỉ có thể được truy cập từ bên trong khối đó.
Nếu chúng ta cố gắng truy cập foo
bên ngoài khối, chúng tôi nhận được một ReferenceError
bởi vì nó nằm ngoài phạm vi:
if (true) {
const foo = "foo";
console.log(foo); // "foo"
}
console.log(foo); // Uncaught ReferenceError: foo is not defined
Chặn các câu lệnh ở các dạng khác, chẳng hạn như for
và while
vòng lặp, cũng sẽ tạo ra một phạm vi cho các biến phạm vi khối. Ví dụ, foo
nằm trong phạm vi một thân hàm bên dưới:
function sayFoo() {
const foo = "foo";
console.log(foo);
}
sayFoo(); // "foo"
console.log(foo); // Uncaught ReferenceError: foo is not defined
Phạm vi lồng nhau và chức năng
JavaScript cho phép các khối lồng nhau và do đó phạm vi lồng nhau. Các phạm vi lồng nhau tạo ra một cây phạm vi hoặc chuỗi phạm vi.
Hãy xem xét mã bên dưới, mã này lồng vào nhiều câu lệnh khối:
if (true) {
const foo = "foo";
console.log(foo); // "foo"
if (true) {
const bar = "bar";
console.log(foo); // "foo"
if (true) {
console.log(foo, bar); // "foo bar"
}
}
}
JavaScript cũng cho phép chúng ta lồng các hàm:
function foo(bar) {
function baz() {
console.log(bar);
}
baz();
}
foo("bar"); // "bar"
Như mong đợi, chúng ta có thể truy cập các biến từ phạm vi trực tiếp của chúng (phạm vi mà chúng được khai báo). Chúng ta cũng có thể truy cập các biến từ phạm vi bên trong của chúng (phạm vi lồng trong phạm vi trực tiếp của chúng). Nghĩa là, chúng ta có thể truy cập các biến từ phạm vi mà chúng được khai báo trong và từ mọi phạm vi bên trong.
Trước khi tiếp tục, chúng ta nên làm rõ sự khác biệt trong hành vi này giữa các kiểu khai báo biến.
Phạm vi của let, const và var trong JavaScript
Chúng ta có thể tạo các biến với let
, const
và var
các tờ khai. Vì let
và const
, phạm vi khối hoạt động như đã giải thích ở trên. Tuy nhiên, var
cư xử khác nhau.
để cho và const
let
và const
tạo các biến trong phạm vi khối. Khi được khai báo trong một khối, chúng chỉ có thể truy cập được trong khối đó. Hành vi này đã được thể hiện trong các ví dụ trước đây của chúng tôi:
if (true) {
const foo = "foo";
console.log(foo); // "foo"
}
console.log(foo); // Uncaught ReferenceError: foo is not defined
var
Các biến được tạo với var
được đặt trong phạm vi chức năng gần nhất của chúng hoặc phạm vi toàn cầu (chúng ta sẽ thảo luận ngay sau đây). Chúng không nằm trong phạm vi khối:
function foo() {
if (true) {
var foo = "foo";
}
console.log(foo);
}
foo(); // "foo"
var
có thể tạo ra các tình huống khó hiểu và thông tin này chỉ được đưa vào để đảm bảo tính đầy đủ. Tốt nhất là sử dụng let
và const
khi có thể. Phần còn lại của bài viết này sẽ chỉ liên quan đến let
và const
biến.
Nếu bạn quan tâm đến cách var
trong ví dụ trên, bạn nên xem bài viết của tôi về cẩu.
Phạm vi toàn cầu và mô-đun trong JavaScript
Ngoài phạm vi khối, các biến có thể được đặt trong phạm vi toàn cầu và mô-đun.
Trong trình duyệt web, phạm vi toàn cầu nằm ở cấp cao nhất của tập lệnh. Nó là gốc của cây phạm vi mà chúng tôi đã mô tả trước đó và nó chứa tất cả các phạm vi khác. Do đó, việc tạo một biến trong phạm vi toàn cầu giúp nó có thể truy cập được ở mọi phạm vi:
<script>
const foo = "foo";
</script>
<script>
console.log(foo); // "foo"
function bar() {
if (true) {
console.log(foo);
}
}
bar(); // "foo"
</script>
Mỗi mô-đun cũng có phạm vi riêng của nó. Các biến được khai báo ở cấp độ mô-đun chỉ khả dụng trong mô-đun đó – chúng không phải là toàn cầu:
<script type="module">
const foo = "foo";
</script>
<script>
console.log(foo); // Uncaught ReferenceError: foo is not defined
</script>
Bao đóng trong JavaScript
Bây giờ chúng ta đã hiểu phạm vi, hãy quay lại ví dụ mà chúng ta đã thấy trong phần giới thiệu:
function sayWord(word) {
return () => console.log(word);
}
const sayHello = sayWord("hello");
sayHello(); // "hello"
Nhớ lại rằng có hai điểm thú vị về ví dụ này:
- Hàm trả về từ
sayWord
có thể truy cậpword
tham số - Hàm trả về duy trì giá trị của
word
khi nàosayHello
được gọi bên ngoài phạm vi củaword
Điểm đầu tiên có thể được giải thích bằng phạm vi từ vựng: hàm được trả về có thể truy cập word
bởi vì nó tồn tại trong phạm vi bên ngoài của nó.
Điểm thứ hai là do các bao đóng: Một bao đóng là một hàm được kết hợp với các tham chiếu đến các biến được xác định bên ngoài nó. Các bao đóng duy trì các tham chiếu biến, cho phép các hàm truy cập các biến bên ngoài phạm vi của chúng. Chúng “đính kèm” hàm và các biến trong môi trường của nó.
Ví dụ về Bao đóng trong JavaScript
Bạn có thể đã gặp và sử dụng bao đóng thường xuyên mà không hề hay biết. Hãy khám phá một số cách khác để sử dụng bao đóng.
gọi lại
Thông thường, một cuộc gọi lại sẽ tham chiếu đến một biến được khai báo bên ngoài chính nó. Ví dụ:
function getCarsByMake(make) {
return cars.filter(x => x.make === make);
}
make
có sẵn trong cuộc gọi lại vì phạm vi từ vựng và giá trị của make
được duy trì khi chức năng ẩn danh được gọi bởi filter
vì đóng cửa.
trạng thái lưu trữ
Chúng ta có thể sử dụng các bao đóng để trả về các đối tượng từ các hàm lưu trữ trạng thái. Hãy xem xét những điều sau đây makePerson
hàm trả về một đối tượng có thể lưu trữ và thay đổi một name
:
function makePerson(name) {
let _name = name;
return {
setName: (newName) => (_name = newName),
getName: () => _name,
};
}
const me = makePerson("Zach");
console.log(me.getName()); // "Zach"
me.setName("Zach Snoek");
console.log(me.getName()); // "Zach Snoek"
Ví dụ này minh họa cách đóng không chỉ đóng băng các giá trị của biến từ phạm vi bên ngoài của hàm trong quá trình tạo. Thay vào đó, chúng duy trì các tham chiếu trong suốt thời gian đóng.
phương pháp riêng tư
Nếu bạn đã quen thuộc với lập trình hướng đối tượng, bạn có thể nhận thấy rằng ví dụ trước của chúng ta gần giống với một lớp lưu trữ trạng thái riêng tư và hiển thị các phương thức getter và setter công khai. Chúng ta có thể mở rộng song song hướng đối tượng này hơn nữa bằng cách sử dụng các bao đóng để triển khai các phương thức riêng tư:
function makePerson(name) {
let _name = name;
function privateSetName(newName) {
_name = newName;
}
return {
setName: (newName) => privateSetName(newName),
getName: () => _name,
};
}
privateSetName
người tiêu dùng không thể truy cập trực tiếp và nó có thể truy cập biến trạng thái riêng tư _name
thông qua một đóng cửa.
Phản ứng xử lý sự kiện
Cuối cùng, bao đóng là phổ biến trong trình xử lý sự kiện React. Sau đây Counter
thành phần được sửa đổi từ tài liệu React:
function Counter({ initialCount }) {
const [count, setCount] = React.useState(initialCount);
return (
<>
<button onClick={() => setCount(initialCount)}>Reset</button>
<button onClick={() => setCount((prevCount) => prevCount - 1)}>
-
</button>
<button onClick={() => setCount((prevCount) => prevCount + 1)}>
+
</button>
<button onClick={() => alert(count)}>Show count</button>
</>
);
}
function App() {
return <Counter initialCount={0} />;
}
Đóng cửa làm cho nó có thể cho:
- trình xử lý nhấp vào nút đặt lại, giảm và tăng để truy cập
setCount
- nút đặt lại để truy cập
initialCount
từCounter
đạo cụ của - và nút “Hiển thị số lượng” để hiển thị
count
tiểu bang.
Closure rất quan trọng trong các phần khác của React, chẳng hạn như props và hook. Thảo luận về những chủ đề này nằm ngoài phạm vi của bài viết này. Tôi khuyên bạn nên đọc bài đăng này từ Kent C. Dodds hoặc bài đăng này từ Dan Abramov để tìm hiểu thêm về vai trò của các lần đóng trong React.
Phần kết luận
Phạm vi đề cập đến một phần của chương trình mà chúng ta có thể truy cập vào một biến. JavaScript cho phép chúng ta lồng các phạm vi và các biến được khai báo ở phạm vi bên ngoài có thể truy cập được từ tất cả các phạm vi bên trong. Các biến có thể ở phạm vi toàn cầu, mô-đun hoặc khối.
Bao đóng là một hàm kèm theo các tham chiếu đến các biến trong phạm vi bên ngoài của nó. Bao đóng cho phép các hàm duy trì kết nối với các biến bên ngoài, thậm chí bên ngoài phạm vi của các biến.
Có nhiều cách sử dụng bao đóng, từ việc tạo các cấu trúc giống như lớp lưu trữ trạng thái và triển khai các phương thức riêng tư để chuyển các cuộc gọi lại tới các trình xử lý sự kiện.
Kết nối nào
Nếu bạn quan tâm đến nhiều bài viết như thế này, hãy đăng ký nhận bản tin của tôi và kết nối với tôi trên LinkedIn và Twitter!
Sự nhìn nhận
Cảm ơn Bryan Smith đã cung cấp phản hồi về bản nháp của bài đăng này.
Ảnh bìa của Karine Avetisyan trên Bapt.