JavaScript 함수: 범위, 클로저 및 상위 순서 함수

작성자

카테고리:

← 피드로
DEV Community · Dini Anjelina · 2026-09-23 개발(SW)

In the previous article, we explored the basic concepts of JavaScript functions — how to define a function, pass arguments, return values, and use arrow functions.

But understanding how to write and call a function is only the beginning.

Have you ever wondered why a function can access a variable declared outside it?

Or why a function can still access a variable even after the function that created it has already finished executing?

And how can a function be passed as an argument to another function?

These behaviors are closely related to three important JavaScript concepts: scope, closures, and higher-order functions.

In this article, we’ll explore these concepts step by step and show how they connect through practical examples.

1. Understanding Scope

Scope determines where a variable can be accessed in your code.

For example:

const name = "Dini";

function greet() {
    console.log(name);
}

greet();

Enter fullscreen mode Exit fullscreen mode

The greet() function can access name even though name was not declared inside the function.
This works because name is declared in an outer scope.

Now consider this example:

function greet() {
    const message = "Hello, Dini!";

    console.log(message); // "Hello, Dini!"
}

greet();

console.log(message); // ReferenceError: message is not defined

Enter fullscreen mode Exit fullscreen mode

The first console.log() works because message is declared inside greet(). However, the second one causes an error because message only exists within the function’s scope.

Global Scope
└── greet()
    └── message

Enter fullscreen mode Exit fullscreen mode

The variable message is accessible from inside greet(), but it is not accessible from the outside.

This is the basic idea of scope: a variable is only accessible within the scope where it is defined and the scopes nested inside it.

JavaScript has the following kinds of scopes:

  • Global scope: The default scope for all code running in script mode.
  • Module scope: The scope for code running in module mode.
  • Function scope: The scope created with a function.
  • Block scope: The scope created with a pair of curly braces (a block).

Blocks only include let and const declarations, but not var declarations.

{
  var x = 10;
}
console.log(x); // 10

Enter fullscreen mode Exit fullscreen mode

{
  const x = 10;
}
console.log(x); // ReferenceError: x is not defined

Enter fullscreen mode Exit fullscreen mode

2. Lexical Scope and Scope Chain

JavaScript uses lexical scoping, which means that the accessibility of a variable is determined by where the code is written, not where a function is called.

Consider this example:

const name = "Dini";

function outer() {
    const greeting = "Hello";

    function inner() {
        console.log(greeting);
        console.log(name);
    }

    inner();
}

outer();

Enter fullscreen mode Exit fullscreen mode

The inner() function can access both greeting and name.
Why?
Because JavaScript looks for variables through a scope chain.

You can think of the relationship like this:

inner()
   ↓
outer()
   ↓
Global Scope

Enter fullscreen mode Exit fullscreen mode

When JavaScript encounters:

console.log(greeting);

Enter fullscreen mode Exit fullscreen mode

It first looks for greeting inside inner().
If it doesn’t find it there, JavaScript looks at the outer scope, which is outer().
It finds greeting there, so the lookup stops.

For:

console.log(name);

Enter fullscreen mode Exit fullscreen mode

JavaScript doesn’t find name inside inner() or outer(), so it continues to the global scope and finds it there.
This is the scope chain.

An important thing to remember is that the relationship only works from inner scope to outer scope.

An inner function can access variables from its outer scopes:
inner → outer → global

This concept is important because it is the foundation for understanding closures.

3. What Is a Closure?

Now that we understand scope and the scope chain, let’s look at a behavior that might seem a little surprising at first.

Consider this example:

function createGreeting() {
    const message = "Hello, Dini!";

    return function () {
        console.log(message);
    };
}

const greet = createGreeting();

greet();

Enter fullscreen mode Exit fullscreen mode

The output is:

Hello, Dini!

Enter fullscreen mode Exit fullscreen mode

At first glance, you might wonder:

How can greet() still access message?

The createGreeting() function has already finished executing.

Normally, we might expect the variables inside createGreeting() to no longer be accessible from the outside.

But greet() can still access message.

This is where closures come into play.

A Function Remembers Its Surrounding Scope

When JavaScript creates the function returned by createGreeting(), that function keeps access to the variables available in the scope where it was created.

In this example:

function createGreeting() {
    const message = "Hello, Dini!";

    return function () {
        console.log(message);
    };
}

Enter fullscreen mode Exit fullscreen mode

The inner function uses message, which belongs to the outer function’s scope.

When we do this:

const greet = createGreeting();

Enter fullscreen mode Exit fullscreen mode

createGreeting() runs and returns the inner function.

We can think of the result as:

greet
  ↓
function () {
    console.log(message);
}
  ↓
remembers access to
  ↓
message = "Hello, Dini!"

Enter fullscreen mode Exit fullscreen mode

So even though createGreeting() has finished executing, the returned function still has access to the environment where it was created.

This behavior is called a closure.

A Simple Definition

A closure is created when a function retains access to variables from its surrounding lexical scope, even after the outer function has finished executing.

In simpler words:

A closure allows a function to remember and access variables from the scope where it was created.

The important part is that the function doesn’t simply remember the value as a copy. It retains access to the variable itself.

We can see this more clearly with a counter.

Closures Can Preserve State

Consider this example:

function createCounter() {
    let count = 0;

    return function () {
        count++;
        return count;
    };
}

const counter = createCounter();

console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Enter fullscreen mode Exit fullscreen mode

Notice that count is declared inside createCounter().

After this line:

const counter = createCounter();

Enter fullscreen mode Exit fullscreen mode

the createCounter() function has already finished executing.

Yet every time we call counter():

counter();

Enter fullscreen mode Exit fullscreen mode

the value of count is still available and continues from its previous value.

The first call returns:

1

Enter fullscreen mode Exit fullscreen mode

The second:

2

Enter fullscreen mode Exit fullscreen mode

And the third:

3

Enter fullscreen mode Exit fullscreen mode

This happens because the returned function forms a closure over the count variable.

We can visualize it like this:

createCounter()
│
├── count = 0
│
└── returns function
       │
       └── closes over count
              ↓
          counter()
              ↓
          count = 1
              ↓
          counter()
              ↓
          count = 2
              ↓
          counter()
              ↓
          count = 3

Enter fullscreen mode Exit fullscreen mode

The closure allows counter() to maintain access to count between function calls.

Closures Create Independent State

Here’s another interesting part.

What happens if we create two counters?

function createCounter() {
    let count = 0;

    return function () {
        count++;
        return count;
    };
}

const counterA = createCounter();
const counterB = createCounter();

console.log(counterA()); // 1
console.log(counterA()); // 2

console.log(counterB()); // 1
console.log(counterB()); // 2

Enter fullscreen mode Exit fullscreen mode

Even though both counters are created from the same function, they maintain separate values.

counterA → count = 2

counterB → count = 2

Enter fullscreen mode Exit fullscreen mode

They don’t share the same count variable.

Each call to createCounter() creates a new lexical environment, and the returned function closes over its own environment.

This is one of the powerful aspects of closures: they can be used to create independent, persistent state.

Why Are Closures Useful?

Closures are not just an interesting JavaScript behavior. They are useful in many practical situations, such as:

  • Maintaining state between function calls
  • Creating function factories
  • Encapsulating data
  • Creating callbacks that need access to surrounding variables
  • Working with asynchronous operations

For example, a function factory can use closures to create customized functions:

function createMultiplier(multiplier) {
    return function (number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Enter fullscreen mode Exit fullscreen mode

Here, double remembers:

multiplier = 2

Enter fullscreen mode Exit fullscreen mode

while triple remembers:

multiplier = 3

Enter fullscreen mode Exit fullscreen mode

The same createMultiplier() function can therefore create different functions with their own preserved state.

And that’s the key idea behind closures:

A function can carry access to the environment where it was created.

4. Functions as Values

So far, we’ve seen that functions can access variables from their surrounding scope and that closures can preserve access to those variables.

But there’s another important characteristic of JavaScript functions:

Functions are values.

This means a function can be treated like other values in JavaScript. We can store a function in a variable, pass it to another function, or even return it from a function.

Let’s look at each case.

Assigning a Function to a Variable

We’ve already seen this pattern before:

const greet = function () {
    console.log("Hello!");
};

greet();

Enter fullscreen mode Exit fullscreen mode

Here, the function is assigned to the greet variable.

The variable doesn’t contain the result of calling the function. Instead, it contains a reference to the function itself.

That’s why we use:

greet();

Enter fullscreen mode Exit fullscreen mode

to execute it.

Compare this with:

const message = "Hello!";

console.log(message);

Enter fullscreen mode Exit fullscreen mode

In this example, message stores a string value.

Similarly:

const greet = function () {
    console.log("Hello!");
};

Enter fullscreen mode Exit fullscreen mode

greet stores a function value.

We can also assign the same function to another variable:

const greet = function () {
    console.log("Hello!");
};

const sayHello = greet;

sayHello();

Enter fullscreen mode Exit fullscreen mode

Both greet and sayHello refer to the same function.

This is possible because functions can be treated as values.

Passing a Function as an Argument

Functions can also be passed to other functions.

For example:

function greet(name) {
    console.log(`Hello, ${name}!`);
}

function processUser(name, callback) {
    callback(name);
}

processUser("Dini", greet);

Enter fullscreen mode Exit fullscreen mode

Here, greet is passed as an argument to processUser().

Notice that we pass:

greet

Enter fullscreen mode Exit fullscreen mode

not:

greet()

Enter fullscreen mode Exit fullscreen mode

These two are different.

processUser("Dini", greet);

Enter fullscreen mode Exit fullscreen mode

passes the function itself.

While:

processUser("Dini", greet());

Enter fullscreen mode Exit fullscreen mode

would execute greet() immediately and pass its return value instead.

When a function is passed to another function to be executed later, it is commonly called a callback function.

We’ll explore callbacks in more detail when we discuss higher-order functions.

Returning a Function from Another Function

Functions can also return other functions.

For example:

function createGreeting(name) {
    return function () {
        console.log(`Hello, ${name}!`);
    };
}

const greetDini = createGreeting("Dini");

greetDini();

Enter fullscreen mode Exit fullscreen mode

Here, createGreeting() returns a function.

We then store that returned function in:

const greetDini = createGreeting("Dini");

Enter fullscreen mode Exit fullscreen mode

and execute it later:

greetDini();

Enter fullscreen mode Exit fullscreen mode

This example should look familiar because we saw something similar when discussing closures.

In fact, these two concepts are closely connected.

The returned function is not only a function value. It also forms a closure over the name variable.

That’s why this works:

const greetDini = createGreeting("Dini");
const greetNaufal = createGreeting("Naufal");

greetDini(); // Hello, Dini!
greetNaufal(); // Hello, Naufal!

Enter fullscreen mode Exit fullscreen mode

Each returned function has access to the name from its own surrounding scope.

Functions Can Be Stored, Passed, and Returned

At this point, we can summarize the three things we’ve seen:

Functions can be...

1. Stored in variables
        ↓
   const greet = function () {};

2. Passed as arguments
        ↓
   processUser("Dini", greet);

3. Returned from functions
        ↓
   return function () {};

Enter fullscreen mode Exit fullscreen mode

This flexibility is one of the reasons functions are so powerful in JavaScript.

Functions that can be treated like other values are commonly described as first-class functions.

This doesn’t mean that functions are literally the same as primitive values such as strings or numbers. Rather, JavaScript allows functions to be used as values in these ways.

And this brings us to the next concept.

If a function can receive another function as an argument or return a function, what do we call a function that works with other functions?

That’s where higher-order functions come in.

5. What Is a Higher-Order Function?

In the previous section, we learned that functions can be treated as values. They can be stored in variables, passed as arguments, and returned from other functions.

This ability leads us to another important concept: higher-order functions.

What Is a Higher-Order Function?

A higher-order function is a function that does at least one of the following:

  • Takes another function as an argument
  • Returns a function as its result
  • Or does both

Let’s start with a simple example.

function processNumber(number, operation) {
    return operation(number);
}

function double(number) {
    return number * 2;
}

const result = processNumber(5, double);

console.log(result); // 10

Enter fullscreen mode Exit fullscreen mode

Here, processNumber() receives double as an argument.

Because processNumber() works with another function, it is a higher-order function.

We can visualize it like this:

processNumber()
      │
      ├── number = 5
      │
      └── operation = double
                      │
                      ↓
                  double(5)
                      │
                      ↓
                     10

Enter fullscreen mode Exit fullscreen mode

The important thing to notice is that processNumber() doesn’t need to know exactly what operation does.

It simply receives a function and executes it:

return operation(number);

Enter fullscreen mode Exit fullscreen mode

This makes the function more flexible and reusable.

Higher-Order Function vs Callback

These two concepts are closely related, but they describe different things.

Consider this example:

function processNumber(number, operation) {
    return operation(number);
}

function double(number) {
    return number * 2;
}

processNumber(5, double);

Enter fullscreen mode Exit fullscreen mode

Here:

  • processNumber() is the higher-order function
  • double() is the callback function

Why?

Because processNumber() accepts another function as an argument.

Meanwhile, double() is the function being passed to and called by processNumber().

So we can think of the relationship like this:

Higher-order function
        │
        │ receives
        ↓
Callback function

Enter fullscreen mode Exit fullscreen mode

A callback is simply a function that is passed to another function to be used later.

A higher-order function is a function that receives or returns another function.

Higher-Order Functions Can Return Functions

Remember the example from the closure section?

function createMultiplier(multiplier) {
    return function (number) {
        return number * multiplier;
    };
}

const double = createMultiplier(2);
const triple = createMultiplier(3);

console.log(double(5)); // 10
console.log(triple(5)); // 15

Enter fullscreen mode Exit fullscreen mode

createMultiplier() is also a higher-order function.

Why?

Because it returns another function.

createMultiplier(2)
        ↓
   returns function
        ↓
      double

Enter fullscreen mode Exit fullscreen mode

And:

createMultiplier(3)
        ↓
   returns function
        ↓
      triple

Enter fullscreen mode Exit fullscreen mode

This example also demonstrates how higher-order functions and closures can work together.

The returned function closes over the multiplier variable:

function createMultiplier(multiplier) {
    return function (number) {
        return number * multiplier;
    };
}

Enter fullscreen mode Exit fullscreen mode

So double remembers multiplier = 2, while triple remembers multiplier = 3.

This is a good example of how different JavaScript concepts can work together rather than existing independently.

Why Are Higher-Order Functions Useful?

Higher-order functions allow us to write code that is more flexible, reusable, and composable.

Instead of creating a separate function for every possible operation, we can create a general function and provide the specific behavior when we need it.

For example:

function calculate(number, operation) {
    return operation(number);
}

const double = calculate(10, number => number * 2);
const square = calculate(10, number => number ** 2);

console.log(double); // 20
console.log(square); // 100

Enter fullscreen mode Exit fullscreen mode

The calculate() function doesn’t need to know whether we want to double, square, or perform some other operation.

We simply provide the behavior as a function.

This pattern is extremely common in JavaScript.

In fact, you’ve probably already used higher-order functions without realizing it.

Consider:

const numbers = [1, 2, 3, 4, 5];

const doubled = numbers.map(number => number * 2);

Enter fullscreen mode Exit fullscreen mode

The map() method receives a function:

number => number * 2

Enter fullscreen mode Exit fullscreen mode

That function is used to determine how each item should be transformed.

map() is therefore a higher-order function.

And number => number * 2 is the callback.

The same idea appears in many other array methods, such as:

filter()
forEach()
reduce()
find()
some()
every()

Enter fullscreen mode Exit fullscreen mode

These methods allow us to provide functions that describe what should happen to each item.

We’ll explore these examples in the next section.

6. Higher-Order Functions in Array Methods

Now that we understand what higher-order functions are, let’s look at where we commonly use them in everyday JavaScript.

One of the most common places is array methods.

Methods such as map(), filter(), forEach(), and reduce() accept functions as arguments. This makes them higher-order functions.

Let’s use the same dataset throughout this section:

const users = [
    { name: "Dini", age: 25, active: true },
    { name: "Naufal", age: 30, active: false },
    { name: "Nabil", age: 28, active: true },
];

Enter fullscreen mode Exit fullscreen mode

map()

The map() method creates a new array by transforming each item in the original array.

For example, if we only need the names of our users:

const names = users.map(user => user.name);

console.log(names);

Enter fullscreen mode Exit fullscreen mode

The result is:

["Dini", "Naufal", "Nabil"]

Enter fullscreen mode Exit fullscreen mode

Here:

user => user.name

Enter fullscreen mode Exit fullscreen mode

is the callback function.

map() calls this function for each item in the array.

We can think of the process like this:

users
  ↓
Dini  → "Dini"
Naufal  → "Naufal"
Nabil  → "Nabil"
  ↓
["Dini", "Naufal", "Nabil"]

Enter fullscreen mode Exit fullscreen mode

Because map() receives a function as an argument, map() is a higher-order function.

A useful rule of thumb is:

Use map() when you want to transform every item into something else.

For example:

const ages = users.map(user => user.age);

Enter fullscreen mode Exit fullscreen mode

This produces:

[25, 30, 28]

Enter fullscreen mode Exit fullscreen mode

filter()

While map() transforms items, filter() selects items that satisfy a condition.

For example, we can get only active users:

const activeUsers = users.filter(user => user.active);

console.log(activeUsers);

Enter fullscreen mode Exit fullscreen mode

The result is:

[
    { name: "Dini", age: 25, active: true },
    { name: "Nabil", age: 28, active: true }
]

Enter fullscreen mode Exit fullscreen mode

The callback:

user => user.active

Enter fullscreen mode Exit fullscreen mode

returns a boolean value.

If the result is true, the item is included in the new array.

If the result is false, the item is excluded.

We can visualize it like this:

Dini  → true  → included
Naufal  → false → excluded
Nabil  → true  → included

Enter fullscreen mode Exit fullscreen mode

So a useful rule is:

Use filter() when you want to select some items from an array.

For example:

const adults = users.filter(user => user.age >= 28);

Enter fullscreen mode Exit fullscreen mode

This returns:

[
    { name: "Naufal", age: 30, active: false },
    { name: "Nabil", age: 28, active: true }
]

Enter fullscreen mode Exit fullscreen mode

forEach()

forEach() also accepts a callback function, but its purpose is different.

Instead of creating a new array, forEach() executes a function for each item.

For example:

users.forEach(user => {
    console.log(user.name);
});

Enter fullscreen mode Exit fullscreen mode

The output is:

Dini
Naufal
Nabil

Enter fullscreen mode Exit fullscreen mode

The callback runs once for every item:

Dini  → callback()
Naufal  → callback()
Nabil  → callback()

Enter fullscreen mode Exit fullscreen mode

A common use case is performing an action for each item, such as logging information or triggering another operation.

A useful rule is:

Use forEach() when you want to perform an action for each item and don’t need a new array as the result.

One important difference is that forEach() does not return a new array like map() does.

For example:

const result = users.forEach(user => user.name);

console.log(result); // undefined

Enter fullscreen mode Exit fullscreen mode

If your goal is to create a new array from the items, map() is usually the appropriate method.

reduce()

reduce() is slightly different from the previous methods.

Instead of simply transforming or filtering items, reduce() allows us to combine multiple values into a single result.

For example, we can calculate the total age of all users:

const totalAge = users.reduce((total, user) => {
    return total + user.age;
}, 0);

console.log(totalAge); // 83

Enter fullscreen mode Exit fullscreen mode

Let’s break this down.

The second argument:

0

Enter fullscreen mode Exit fullscreen mode

is the initial value of the accumulator.

The callback receives:

(total, user)

Enter fullscreen mode Exit fullscreen mode

where:

  • total is the accumulated value
  • user is the current item

The process looks like this:

Initial total = 0

0 + 25 = 25
25 + 30 = 55
55 + 28 = 83

Final result = 83

Enter fullscreen mode Exit fullscreen mode

So a useful rule is:

Use reduce() when you want to combine an array into a single result.

That result doesn’t have to be a number.

For example, we could count active users:

const activeUserCount = users.reduce((count, user) => {
    return user.active ? count + 1 : count;
}, 0);

console.log(activeUserCount); // 2

Enter fullscreen mode Exit fullscreen mode

Or even build another object:

const usersByName = users.reduce((result, user) => {
    result[user.name] = user;
    return result;
}, {});

console.log(usersByName);

Enter fullscreen mode Exit fullscreen mode

Output:

{
    "Dini": {
        "name": "Dini",
        "age": 25,
        "active": true
    },
    "Naufal": {
        "name": "Naufal",
        "age": 30,
        "active": false
    },
    "Nabil": {
        "name": "Nabil",
        "age": 28,
        "active": true
    }
}

Enter fullscreen mode Exit fullscreen mode

Seeing the Pattern

At this point, we can compare the methods:

Method What it does Returns map() Transforms every item New array filter() Selects matching items New array forEach() Performs an action for each item undefined reduce() Combines items into one result A single accumulated result

Even though these methods have different purposes, they share an important pattern:

array.method(callback);

Enter fullscreen mode Exit fullscreen mode

For example:

users.map(user => user.name);

users.filter(user => user.active);

users.forEach(user => console.log(user.name));

users.reduce((total, user) => total + user.age, 0);

Enter fullscreen mode Exit fullscreen mode

The array method controls how the array is processed, while the callback provides what should happen to each item.

That’s the power of higher-order functions.

Instead of writing the iteration logic ourselves, we can provide a function that describes the behavior we want.

For example, without filter(), we might write:

const activeUsers = [];

for (const user of users) {
    if (user.active) {
        activeUsers.push(user);
    }
}

Enter fullscreen mode Exit fullscreen mode

With filter(), we can express the same intention more directly:

const activeUsers = users.filter(user => user.active);

Enter fullscreen mode Exit fullscreen mode

The second version focuses more on what we want — active users — rather than how to iterate through the array.

This is one reason higher-order functions are so common in modern JavaScript code.

9. Conclusion

In this article, we explored several JavaScript concepts that are closely connected to how functions work.

The important thing is to understand how these concepts connect:

  • Scope: determines variable accessibility.
  • Lexical Scope & Scope Chain: determines where JavaScript looks for variables.
  • Closure: allows a function to retain access to its surrounding scope.
  • Functions as Values: allows functions to be stored, passed, and returned.
  • Higher-Order Functions: allows functions to work with other functions.

References

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Functions
https://developer.mozilla.org/en-US/docs/Glossary/Scope
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Closures

원문에서 계속 ↗