jquery 콜백에서 호출 될 때 TypeScript "this"범위 지정 문제


107

TypeScript에서 "this"범위 지정을 처리하는 가장 좋은 방법을 모르겠습니다.

다음은 TypeScript로 변환하는 코드의 일반적인 패턴의 예입니다.

class DemonstrateScopingProblems {
    private status = "blah";
    public run() {
        alert(this.status);
    }
}

var thisTest = new DemonstrateScopingProblems();
// works as expected, displays "blah":
thisTest.run(); 
// doesn't work; this is scoped to be the document so this.status is undefined:
$(document).ready(thisTest.run); 

이제 전화를 ...

$(document).ready(thisTest.run.bind(thisTest));

... 작동합니다. 그러나 그것은 다소 끔찍합니다. 이는 일부 상황에서 코드가 모두 잘 컴파일되고 작동 할 수 있음을 의미하지만 범위를 바인딩하는 것을 잊으면 중단됩니다.

클래스 내에서이를 수행하는 방법을 원하므로 클래스를 사용할 때 "this"의 범위에 대해 걱정할 필요가 없습니다.

어떤 제안?

최신 정보

작동하는 또 다른 접근 방식은 뚱뚱한 화살표를 사용하는 것입니다.

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}

이것이 유효한 접근 방식입니까?


2
이 도움이 될 것입니다 : youtube.com/watch?v=tvocUcbCupA
basarat

참고 : Ryan은 TypeScript Wiki에 대한 답변을 복사했습니다 .
Franklin Yu

TypeScript 2+ 솔루션을 찾으 십시오 .
Deilan

답변:


166

여기에는 몇 가지 옵션이 있으며 각각 고유 한 장단점이 있습니다. 불행히도 확실한 최상의 솔루션은 없으며 실제로 응용 프로그램에 따라 다릅니다.

자동 클래스 바인딩
질문에 표시된대로 :

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • 좋음 / 나쁨 : 클래스의 인스턴스 당 메서드 당 추가 클로저를 만듭니다. 이 메서드가 일반적으로 일반 메서드 호출에서만 사용되는 경우 이는 과잉입니다. 그러나 콜백 위치에서 많이 사용되는 경우 this각 호출 사이트가 호출시 새 클로저를 만드는 대신 클래스 인스턴스가 컨텍스트 를 캡처하는 것이 더 효율적입니다 .
  • 좋음 : 외부 호출자가 this컨텍스트 처리를 잊어 버릴 수 없음
  • 좋음 : TypeScript의 Typesafe
  • 좋음 : 함수에 매개 변수가있는 경우 추가 작업 없음
  • 나쁨 : 파생 클래스는 다음을 사용하여 이러한 방식으로 작성된 기본 클래스 메서드를 호출 할 수 없습니다. super.
  • 나쁨 : 메서드가 "사전 바인딩"되고 클래스와 해당 소비자간에 형식이 안전하지 않은 추가 계약을 생성하지 않는 정확한 의미 체계.

Function.bind
또한 다음과 같이 표시됩니다.

$(document).ready(thisTest.run.bind(thisTest));
  • 좋음 / 나쁨 : 첫 번째 방법과 비교하여 메모리 / 성능 균형이 맞지 않습니다.
  • 좋음 : 함수에 매개 변수가있는 경우 추가 작업 없음
  • 나쁨 : TypeScript에서는 현재 유형 안전성이 없습니다.
  • 나쁨 : 중요한 경우 ECMAScript 5에서만 사용 가능
  • 나쁨 : 인스턴스 이름을 두 번 입력해야합니다.


TypeScript의 뚱뚱한 화살표 (설명을 위해 몇 가지 더미 매개 변수와 함께 여기에 표시됨) :

$(document).ready((n, m) => thisTest.run(n, m));
  • 좋음 / 나쁨 : 첫 번째 방법과 비교하여 메모리 / 성능 균형이 맞지 않습니다.
  • 좋음 : TypeScript에서는 100 % 유형 안전성이 있습니다.
  • 좋음 : ECMAScript 3에서 작동
  • 좋음 : 인스턴스 이름을 한 번만 입력하면됩니다.
  • 나쁨 : 매개 변수를 두 번 입력해야합니다.
  • 나쁨 : 가변 매개 변수와 함께 작동하지 않습니다.

1
+1 Great answer Ryan, 장단점의 고장을 좋아합니다. 감사합니다!
Jonathan Moffatt

-Function.bind에서 이벤트를 첨부해야 할 때마다 새 클로저를 만듭니다.
131.

1
뚱뚱한 화살이 방금 해냈습니다! : D : D = () => 감사합니다! : D
크리스토퍼 증권

@ ryan-cavanaugh 개체가 해제되는 시점의 장점과 단점은 어떻습니까? 30 분 이상 활성 상태 인 SPA의 예에서와 같이 JS 가비지 수집기가 처리하기에 가장 적합한 것은 무엇입니까?
abbaf33f

이 모든 것은 클래스 인스턴스가 해제 가능할 때 해제 가능합니다. 이벤트 핸들러의 수명이 더 짧으면 후자의 두 개는 더 일찍 해제 할 수 있습니다. 일반적으로 나는 측정 가능한 차이가 없을 것이라고 말하고 싶습니다.
Ryan Cavanaugh

16

몇 가지 초기 설정이 필요하지만 말 그대로 한 단어로 된 구문으로 무적의 효과 를 낼 수있는 또 다른 솔루션은 메소드 데코레이터 를 사용하여 getter를 통해 메소드를 JIT 바인딩하는 것입니다.

이 아이디어의 구현을 보여주기 위해 GitHub저장소를 만들었습니다 (주석을 포함하여 40 줄의 코드로 답변에 맞추기에는 약간 길어짐). 다음과 같이 간단하게 사용할 수 있습니다.

class DemonstrateScopingProblems {
    private status = "blah";

    @bound public run() {
        alert(this.status);
    }
}

아직 어디에서도 언급되지 않았지만 완벽하게 작동합니다. 또한이 접근 방식에는 눈에 띄는 단점이 없습니다. 런타임 유형 안전성을위한 일부 유형 검사를 포함 하여이 데코레이터의 구현 은 사소하고 간단하며 초기 메서드 호출 후 본질적으로 오버 헤드가 0입니다.

중요한 부분은 첫 번째 호출 직전 에 실행되는 클래스 프로토 타입에 다음 getter를 정의하는 것입니다 .

get: function () {
    // Create bound override on object instance. This will hide the original method on the prototype, and instead yield a bound version from the
    // instance itself. The original method will no longer be accessible. Inside a getter, 'this' will refer to the instance.
    var instance = this;

    Object.defineProperty(instance, propKey.toString(), {
        value: function () {
            // This is effectively a lightweight bind() that skips many (here unnecessary) checks found in native implementations.
            return originalMethod.apply(instance, arguments);
        }
    });

    // The first invocation (per instance) will return the bound method from here. Subsequent calls will never reach this point, due to the way
    // JavaScript runtimes look up properties on objects; the bound method, defined on the instance, will effectively hide it.
    return instance[propKey];
}

전체 소스


이 아이디어는 클래스 데코레이터에서이 작업을 대신 수행하고 메서드를 반복하고 각 메서드에 대해 위의 속성 설명자를 한 번에 정의함으로써 한 단계 더 나아갈 수 있습니다.


내가 필요한 것만!
Marcel van der Drift

14

네크 로맨싱.
화살표 함수 (화살표 함수는 30 % 더 느림) 또는 게터를 통한 JIT 메서드가 필요하지 않은 명백한 간단한 솔루션이 있습니다.
이 솔루션은 생성자에서 this-context를 바인딩하는 것입니다.

class DemonstrateScopingProblems 
{
    constructor()
    {
        this.run = this.run.bind(this);
    }


    private status = "blah";
    public run() {
        alert(this.status);
    }
}

클래스 생성자의 모든 함수를 자동으로 바인딩하는 autobind 메서드를 작성할 수 있습니다.

class DemonstrateScopingProblems 
{

    constructor()
    { 
        this.autoBind(this);
    }
    [...]
}


export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {
        const val = self[key];

        if (key !== 'constructor' && typeof val === 'function')
        {
            // console.log(key);
            self[key] = val.bind(self);
        } // End if (key !== 'constructor' && typeof val === 'function') 

    } // Next key 

    return self;
} // End Function autoBind

autobind-function을 멤버 함수와 동일한 클래스에 autoBind(this);넣지 않으면this.autoBind(this);

또한 위의 autoBind 함수는 원칙을 보여주기 위해 벙어리됩니다.
이것이 안정적으로 작동하려면 함수가 속성의 getter / setter인지 테스트해야합니다. 그렇지 않으면-boom-클래스에 속성이 포함되어 있으면 즉,

이렇게 :

export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {

        if (key !== 'constructor')
        {
            // console.log(key);

            let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);

            if (desc != null)
            {
                let g = desc.get != null;
                let s = desc.set != null;

                if (g || s)
                {
                    if (g)
                        desc.get = desc.get.bind(self);

                    if (s)
                        desc.set = desc.set.bind(self);

                    Object.defineProperty(self.constructor.prototype, key, desc);
                    continue; // if it's a property, it can't be a function 
                } // End if (g || s) 

            } // End if (desc != null) 

            if (typeof (self[key]) === 'function')
            {
                let val = self[key];
                self[key] = val.bind(self);
            } // End if (typeof (self[key]) === 'function') 

        } // End if (key !== 'constructor') 

    } // Next key 

    return self;
} // End Function autoBind

"this.autoBind (this)"가 아닌 "autoBind (this)"를
사용해야했습니다.

@JohnOpincar : 예, this.autoBind (this)는 자동 바인딩이 별도의 내보내기가 아닌 클래스 내부에 있다고 가정합니다.
Stefan Steiger 2018 년

지금은 이해. 같은 클래스에 메서드를 넣습니다. 나는 그것을 "유틸리티"모듈에 넣었다.
JohnOpincar

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.