예를 들어 setTimeout
에 객체 메서드를 콜백으로 전달할 때 "losing this
"라는 알려진 문제가 있습니다.
이번 장에서는 이를 해결하는 방법을 살펴보겠습니다.
우리는 이미 this
잃어버린 예를 보았습니다. 메소드가 객체와 별도로 어딘가에 전달되면 this
손실됩니다.
setTimeout
사용하면 다음과 같은 일이 발생할 수 있습니다.
사용자 = { 이름: "John", 안녕하세요() { Alert(`안녕하세요, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // 안녕하세요, 정의되지 않았습니다!
보시다시피 출력에는 this.firstName
으로 "John"이 표시되지 않고 undefined
으로 표시됩니다!
이는 setTimeout
객체와 별도로 user.sayHi
함수를 갖기 때문입니다. 마지막 줄은 다음과 같이 다시 작성할 수 있습니다.
f = user.sayHi로 두세요; setTimeout(f, 1000); // 사용자 컨텍스트 손실
브라우저 내 setTimeout
메소드는 약간 특별합니다. 함수 호출에 대해 this=window
설정합니다(Node.js의 경우 타이머 객체가 this
여기서는 실제로 중요하지 않습니다). 따라서 this.firstName
에 대해 존재하지 않는 window.firstName
가져오려고 시도합니다. 다른 유사한 경우에는 this
으로 undefined
가 됩니다.
작업은 매우 일반적입니다. 호출될 다른 위치(여기서는 스케줄러)에 개체 메서드를 전달하려고 합니다. 올바른 컨텍스트에서 호출되는지 확인하는 방법은 무엇입니까?
가장 간단한 해결책은 래핑 기능을 사용하는 것입니다.
사용자 = { 이름: "John", 안녕하세요() { Alert(`안녕하세요, ${this.firstName}!`); } }; setTimeout(함수() { user.sayHi(); // 안녕, 존! }, 1000);
이제는 외부 어휘 환경에서 user
수신한 다음 정상적으로 메서드를 호출하므로 작동합니다.
동일하지만 더 짧습니다.
setTimeout(() => user.sayHi(), 1000); // 안녕, 존!
괜찮아 보이지만 코드 구조에 약간의 취약점이 나타납니다.
setTimeout
이 트리거되기 전에(1초의 지연이 있습니다!) user
값을 변경하면 어떻게 되나요? 그런데 갑자기 잘못된 개체를 호출하게 됩니다!
사용자 = { 이름: "John", 안녕하세요() { Alert(`안녕하세요, ${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ...사용자의 값이 1초 이내에 변경됩니다. 사용자 = { sayHi() { Alert("setTimeout에 다른 사용자가 있습니다!"); } }; // setTimeout의 다른 사용자!
다음 해결책은 그러한 일이 발생하지 않도록 보장합니다.
함수는 this
해결할 수 있는 내장 메소드 바인딩을 제공합니다.
기본 구문은 다음과 같습니다.
// 더 복잡한 구문은 나중에 나올 예정입니다. letboundFunc = func.bind(context);
func.bind(context)
의 결과는 함수로 호출할 수 있고 this=context
설정하여 func
에 대한 호출을 투명하게 전달하는 특수 함수와 같은 "이국적인 객체"입니다.
즉, boundFunc
호출하는 것은 this
수정한 func
와 같습니다.
예를 들어 여기에서 funcUser
this=user
사용하여 func
에 대한 호출을 전달합니다.
사용자 = { 이름: "John" }; 함수 함수() { 경고(this.firstName); } let funcUser = func.bind(user); funcUser(); // 존
여기서는 func
의 "바운드 변형"인 func.bind(user)
사용하여 this=user
수정했습니다.
모든 인수는 "있는 그대로" 원래 func
에 전달됩니다. 예를 들면 다음과 같습니다.
사용자 = { 이름: "John" }; 함수 func(문구) { Alert(문구 + ', ' + this.firstName); } // 이것을 사용자에게 바인딩 let funcUser = func.bind(user); funcUser("안녕하세요"); // 안녕하세요, John(인수 "Hello"가 전달되고 this=user)
이제 객체 메소드를 사용해 보겠습니다.
사용자 = { 이름: "John", 안녕하세요() { Alert(`안녕하세요, ${this.firstName}!`); } }; let sayHi = user.sayHi.bind(user); // (*) //객체 없이 실행할 수 있음 안녕하세요(); // 안녕, 존! setTimeout(sayHi, 1000); // 안녕, 존! //1초 이내에 user의 값이 변경되더라도 // sayHi는 이전 사용자 객체에 대한 참조인 사전 바인딩된 값을 사용합니다. 사용자 = { sayHi() { Alert("setTimeout에 다른 사용자가 있습니다!"); } };
(*)
줄에서 user.sayHi
메소드를 가져와 user
에 바인딩합니다. sayHi
는 단독으로 호출되거나 setTimeout
에 전달될 수 있는 "바운드" 함수입니다. 상관없습니다. 컨텍스트가 맞을 것입니다.
여기서 인수가 "있는 그대로" 전달되는 것을 볼 수 있으며, this
bind
에 의해 수정됩니다.
사용자 = { 이름: "John", 말하다(문구) { Alert(`${phrase}, ${this.firstName}!`); } }; = user.say.bind(user)라고 합시다. say("안녕하세요"); // 안녕, 존! ("Hello" 인수가 전달되어 말함) say("안녕"); // 안녕, 존! ("Bye"는 다음과 같이 전달됩니다)
편의 방법: bindAll
객체에 많은 메소드가 있고 이를 적극적으로 전달할 계획이라면 모든 메소드를 루프에 바인딩할 수 있습니다.
for (사용자에게 키 입력) { if (사용자[키] 유형 == '함수') { 사용자[키] = 사용자[키].bind(사용자); } }
JavaScript 라이브러리는 편리한 대량 바인딩을 위한 기능도 제공합니다(예: lodash의 _.bindAll(object, methodNames)).
지금까지 우리는 this
바인딩하는 것에 대해서만 이야기했습니다. 한 단계 더 나아가 보겠습니다.
this
뿐만 아니라 인수도 바인딩할 수 있습니다. 거의 수행되지 않지만 때로는 유용할 수 있습니다.
bind
의 전체 구문은 다음과 같습니다.
letbound = func.bind(context, [arg1], [arg2], ...);
컨텍스트를 this
및 함수의 시작 인수로 바인딩할 수 있습니다.
예를 들어, 곱셈 함수 mul(a, b)
있습니다:
함수 mul(a, b) { a * b를 반환; }
bind
사용하여 베이스에 double
함수를 생성해 보겠습니다.
함수 mul(a, b) { a * b를 반환; } let double = mul.bind(null, 2); 경고( double(3) ); // = mul(2, 3) = 6 경고( double(4) ); // = mul(2, 4) = 8 경고( double(5) ); // = mul(2, 5) = 10
mul.bind(null, 2)
에 대한 호출은 mul
에 대한 호출을 전달하는 새로운 함수 double
생성하고 null
컨텍스트로 고정하고 2
첫 번째 인수로 고정합니다. 추가 인수는 "있는 그대로" 전달됩니다.
이를 부분 함수 적용이라고 합니다. 기존 함수의 일부 매개변수를 수정하여 새 함수를 만듭니다.
여기서는 실제로 this
사용하지 않습니다. 하지만 bind
이것이 필요하므로 null
과 같은 것을 입력해야 합니다.
아래 코드의 triple
함수는 값을 3배로 만듭니다.
함수 mul(a, b) { a * b를 반환; } 트리플 = mul.bind(null, 3); 경고(triple(3) ); // = mul(3, 3) = 9 경고(triple(4) ); // = mul(3, 4) = 12 경고(triple(5) ); // = mul(3, 5) = 15
왜 우리는 보통 부분 함수를 만드는가?
장점은 읽을 수 있는 이름( double
, triple
)을 가진 독립적인 함수를 만들 수 있다는 것입니다. 우리는 그것을 사용할 수 있고, bind
으로 수정되었기 때문에 매번 첫 번째 인수를 제공하지 않을 수 있습니다.
다른 경우에는 매우 일반적인 기능이 있고 편의상 덜 보편적인 변형을 원할 때 부분 적용이 유용합니다.
예를 들어, send(from, to, text)
함수가 있습니다. 그런 다음 user
개체 내부에서 현재 사용자가 보내는 sendTo(to, text)
의 부분 변형을 사용할 수 있습니다.
일부 인수를 수정하고 싶지만 this
컨텍스트는 수정하지 않으려면 어떻게 해야 합니까? 예를 들어 객체 메서드의 경우입니다.
기본 bind
이를 허용하지 않습니다. 문맥을 생략하고 논쟁으로 넘어갈 수는 없습니다.
다행스럽게도 인수만 바인딩하는 함수 partial
을 쉽게 구현할 수 있습니다.
이와 같이:
함수 부분(func, ...argsBound) { return function(...args) { // (*) return func.call(this, ...argsBound, ...args); } } // 용법: 사용자 = { 이름: "John", say(시간, 구문) { Alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // 고정된 시간을 갖는 부분 메소드 추가 user.sayNow = 부분(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("안녕하세요"); // 다음과 같습니다: // [10:00] 존: 안녕하세요!
partial(func[, arg1, arg2...])
호출의 결과는 다음을 사용하여 func
호출하는 래퍼 (*)
입니다.
얻는 것과 동일 this
( user.sayNow
의 경우 user
라고 부릅니다).
그런 다음 ...argsBound
– partial
호출의 인수( "10:00"
)를 제공합니다.
그런 다음 ...args
– 래퍼에 제공된 인수( "Hello"
)를 제공합니다.
스프레드 구문을 사용하면 너무 쉽죠?
또한 lodash 라이브러리에는 준비된 _.partial 구현이 있습니다.
func.bind(context, ...args)
메소드는 컨텍스트 this
및 첫 번째 인수(주어진 경우)를 수정하는 함수 func
의 "바운드 변형"을 반환합니다.
일반적으로 우리는 객체 메서드에 대해 this
해결하기 위해 bind
적용하여 어딘가에 전달할 수 있습니다. 예를 들어 setTimeout
입니다.
기존 함수의 일부 인수를 수정하면 결과(덜 보편적인) 함수를 부분 적용 또는 부분이라고 합니다.
동일한 인수를 반복해서 반복하고 싶지 않을 때 부분이 편리합니다. send(from, to)
함수가 있고 from
이 작업에 대해 항상 동일해야 하는 것처럼 부분을 가져와 계속 진행할 수 있습니다.
중요도: 5
결과는 어떻게 될까요?
함수 f() { 경고( 이 ); // ? } 사용자 = { g: f.bind(널) }; user.g();
대답: null
.
함수 f() { 경고( 이 ); // 널 } 사용자 = { g: f.bind(널) }; user.g();
바인딩된 함수의 컨텍스트는 고정되어 있습니다. 더 이상 바꿀 수 있는 방법이 없습니다.
따라서 user.g()
실행하는 동안에도 원래 함수는 this=null
로 호출됩니다.
중요도: 5
추가 바인딩으로 this
변경할 수 있나요?
결과는 어떻게 될까요?
함수 f() { 경고(this.name); } f = f.bind( {이름: "John"} ).bind( {이름: "Ann" } ); 에프();
대답: 존 .
함수 f() { 경고(this.name); } f = f.bind( {이름: "John"} ).bind( {이름: "피트"} ); 에프(); // 존
f.bind(...)
에 의해 반환된 이국적인 바인딩된 함수 객체는 생성 시에만 컨텍스트(및 제공된 경우 인수)를 기억합니다.
함수는 다시 바인딩될 수 없습니다.
중요도: 5
함수의 속성에는 값이 있습니다. bind
후에 변경됩니까? 왜, 아니면 왜 안되는가?
함수 sayHi() { 경고(this.name); } sayHi.test = 5; 바운드 = sayHi.bind({ 이름: "존" }); 경고(bound.test); // 출력은 어떻게 될까요? 왜?
대답은: undefined
.
bind
의 결과는 또 다른 객체입니다. test
속성이 없습니다.
중요도: 5
아래 코드에서 askPassword()
호출은 비밀번호를 확인한 후 답변에 따라 user.loginOk/loginFail
호출해야 합니다.
하지만 오류가 발생합니다. 왜?
모든 것이 올바르게 작동하도록 강조 표시된 줄을 수정합니다(다른 줄은 변경되지 않음).
함수 AskPassword(확인, 실패) { let 비밀번호 = 프롬프트("비밀번호?", ''); if (password == "rockstar") ok(); 그렇지 않으면 실패(); } 사용자 = { 이름: '존', 로그인확인() { Alert(`${this.name}님이 로그인했습니다`); }, 로그인 실패() { Alert(`${this.name} 로그인에 실패했습니다`); }, }; AskPassword(user.loginOk, user.loginFail);
askPassword
객체 없이 loginOk/loginFail
함수를 가져오기 때문에 오류가 발생합니다.
호출할 때 자연스럽게 this=undefined
라고 가정합니다.
컨텍스트를 bind
해 보겠습니다.
함수 AskPassword(확인, 실패) { let 비밀번호 = 프롬프트("비밀번호?", ''); if (password == "rockstar") ok(); 그렇지 않으면 실패(); } 사용자 = { 이름: '존', 로그인확인() { Alert(`${this.name}님이 로그인했습니다`); }, 로그인 실패() { Alert(`${this.name} 로그인에 실패했습니다`); }, }; AskPassword(user.loginOk.bind(사용자), user.loginFail.bind(사용자));
이제 작동합니다.
대체 솔루션은 다음과 같습니다.
//... AskPassword(() => user.loginOk(), () => user.loginFail());
일반적으로 그것도 작동하고 좋아 보입니다.
askPassword
호출된 후 방문자가 응답하고 () => user.loginOk()
호출하기 전에 user
변수가 변경될 수 있는 더 복잡한 상황에서는 신뢰성이 약간 떨어집니다.
중요도: 5
이 작업은 "this"를 잃는 함수 수정의 좀 더 복잡한 변형입니다.
user
개체가 수정되었습니다. 이제 두 개의 함수 loginOk/loginFail
대신 단일 함수 user.login(true/false)
가 있습니다.
user.login(true)
ok
로 호출하고 user.login(false)
fail
로 호출하려면 아래 코드에서 무엇을 askPassword
에 전달해야 합니까?
함수 AskPassword(확인, 실패) { let 비밀번호 = 프롬프트("비밀번호?", ''); if (password == "rockstar") ok(); 그렇지 않으면 실패(); } 사용자 = { 이름: '존', 로그인(결과) { Alert( this.name + (결과 ? ' 로그인됨' : ' 로그인 실패') ); } }; 비밀번호를 물어보세요(?, ?); // ?
변경 사항은 강조 표시된 부분만 수정해야 합니다.
래퍼 함수를 사용하거나 화살표를 간결하게 사용하세요.
AskPassword(() => user.login(true), () => user.login(false));
이제 외부 변수에서 user
가져와 일반적인 방식으로 실행합니다.
또는 user.login
에서 user
컨텍스트로 사용하고 올바른 첫 번째 인수를 갖는 부분 함수를 만듭니다.
AskPassword(user.login.bind(user, true), user.login.bind(user, false));