>  기사  >  웹 프론트엔드  >  자바스크립트 주소 지정, 클로저, 객체 모델 및 관련 문제에 대해 자세히 살펴보기_javascript 기술

자바스크립트 주소 지정, 클로저, 객체 모델 및 관련 문제에 대해 자세히 살펴보기_javascript 기술

WBOY
WBOY원래의
2016-05-16 18:53:381005검색

JS는 동적 언어이기 때문에 JS의 주소 지정은 C처럼 컴파일 후에 결정되는 것이 아니라 현장 주소 지정입니다. 게다가 JS에서는 this 포인터를 도입했는데, 이는 함수에 매개변수로 "암시적으로" 전달되기 때문에 매우 성가신 일입니다. 먼저 "범위 체인" 주제의 예를 살펴보겠습니다.
var testvar = 'window property'
var o1 = {testvar:'1', fun:function(){alert('o1: ' this .testvar);}};
var o2 = {testvar:'2', fun:function(){alert('o2: ' this.testvar);}}; // '1'
o2.fun(); // '2'
o1.fun.call(o2); // '2' 세 가지 경고 결과가 동일하지 않습니다. 그렇지? 사실 모든 흥미롭고 이상한 개념은 하나의 문제로 요약될 수 있으며, 그것이 바로 해결입니다.
단순 변수 주소 지정
JS는 정적 범위인가요, 동적 범위인가요?
안타까운 소식을 전하자면, JS는 정적으로 범위가 지정됩니다. 즉, 변수 주소 지정은 Perl과 같은 동적 범위가 지정되는 언어보다 훨씬 더 복잡합니다. 다음 코드는 프로그래밍 언어 원리의 예입니다.
02| var x = 1
03| eval('f1 = function(){echo(x) } ');
04| 함수 f2(){var x = 2;f1()};
07| > 출력은 1입니다. 이는 pascal 및 ada와 정확히 동일하지만 f1은 eval을 사용하여 동적으로 정의됩니다. 또 다른 예는 프로그래밍 언어 원칙에서 비롯됩니다.
function big2(){
var x = 1
function f2(){echo(x)} //x 값을 사용하여 출력 생성
함수 f3(){var x = 3;f4(f2)};
함수 f4(f){var x = 4;f()}
f3()}
big2();//출력 1: 딥 바인딩; 출력 4: 얕은 바인딩; 출력 3: 특수 바인딩
출력은 여전히 ​​1입니다. 이는 JS가 정적 범위일 뿐만 아니라 딥 바인딩이기도 함을 나타냅니다. 이제 뭔가 잘못되었습니다...
ARI의 개념
함수 실행 시(특히 Ada와 같이 중첩 함수를 허용하는 언어에서) 복잡한 주소 지정 문제를 설명하기 위해 다음과 같이 정의됩니다. "프로그래밍 언어의 원리" "ARI" 책:
함수 주소
로컬 변수
반환 주소
동적 링크
정적 링크
를 포함하여 스택에 있는 일부 레코드입니다. 여기서 동적 링크는 항상 함수의 특정 호출자를 가리킵니다. 예를 들어 b가 실행될 때 a가 호출되고 a의 ARI에서 동적 링크가 b를 가리킬 때 정적 링크가 상위 요소를 설명합니다. a는 함수가 루트 트리로 구성되어 있기 때문에 모든 정적 링크를 요약하면 확실히 호스트(예: 창)를 가리킬 것입니다(주석 뒤의 출력).
var x = 'x in 호스트';
function a(){echo(x)};
function b(){var x = 'x inside b';echo(x)}; ){var x = 'x inside c';a()};
function d() {
var x = 'x inside d,a 클로저로 만든 함수'
return function(){ echo(x)}};
a();// 호스트의 x
b ();// b 내부의 x
c();// 호스트의 x
d()( );// x 내부 d, 클로저로 만들어진 함수가 첫 번째 문장에서 호출됩니다. "스택"에 다음 내용이 있음을 알 수 있습니다(스택의 상단이 왼쪽에 있음):
[ARI of a] → [호스트] A의 정적 링크는 x가 a에 정의되어 있지 않기 때문에 호스트로 바로 이동하고 인터프리터는 x를 찾습니다. x는 b의 지역 변수에 기록되며 최종 에코는 x inside b: 'x inside b'입니다.
이제 c를 호출할 때 스택 정보를 다음과 같이 작성할 수 있습니다. :
동적 체인: [a]→[c]→[Host]
정적 체인: [c]→[Host] [a]→[Host]
x의 주소 지정은 다음에 수행되기 때문입니다. a를 호출하면 정적 링크는 여전히 호스트를 직접 가리킵니다. 당연히 x는 여전히 '호스트의 x'입니다.
d의 상황은 더욱 흥미롭습니다. d는 함수를 반환값으로 생성하고 바로 호출됩니다~ d의 반환값은 d의 수명주기 내에서 생성되기 때문에 d의 반환값은 정적입니다. link는 d를 가리키므로 호출되면 x in d가 출력됩니다: 'x inside d, a closure-made function'.
정적 링크 생성 타이밍
Yueying과 amingoo는 "클로저"가 함수의 "호출 시간 참조"라고 말했습니다. "프로그래밍 언어의 원리"에서는 간단히 ARI라고 부르지만 차이점은 "프로그램이 "디자인 언어의 원리"의 ARI는 스택에 저장되며 함수의 수명 주기가 끝나면 ARI가 삭제되지만 JS 클로저의 경우에는 그렇지 않습니다. 그것과 그 멤버를 가리킨다. (혹은 어떤 코드도 그것을 찾을 수 없다.) 우리는 ARI 함수를 함수의 "옷"에 싸인 객체로 간단히 생각할 수 있습니다.
"프로그래밍 언어의 원리"에서 설명한 정적 체인은 호출될 때 생성됩니다. 그러나 정적 체인 간의 관계는 코드가 컴파일될 때 결정됩니다. 예를 들어, 다음 코드는
PROCEDURE a;
PROCEDURE b;
END
PEOCEDURE c
END
END
에서 b와 c의 정적 링크입니다. . b가 호출되고 b의 변수가 b의 지역 변수에 없으면 컴파일러는 변수나 RTE를 찾을 때까지 정적 체인을 따라 스택을 검색하는 코드 조각을 생성합니다.
ada와 같은 컴파일된 언어와 달리 JS는 완전히 해석되는 언어이며 함수를 동적으로 생성할 수 있어 "정적 체인 유지 관리" 문제가 발생합니다. 다행스럽게도 JS 함수는 직접 수정할 수 없습니다. 이는 erl의 기호와 같습니다. 따라서 정적 링크는 정의될 때마다 업데이트되기만 하면 됩니다.정의 메서드가 function(){}인지 eval 할당인지에 관계없이 정적 체인은 함수가 생성된 후에 고정됩니다.
다시 큰 예로 돌아가 보겠습니다. 인터프리터가 "function big(){...}"을 실행하면 메모리에 함수 인스턴스가 생성되고 이를 호스트에 정적으로 연결합니다. 그러나 마지막 줄에서 호출되면 인터프리터는 메모리에 ARI로 영역을 그립니다. 우리는 ARI[big]가 될 수도 있습니다. 실행 포인터가 라인 2로 이동합니다.
실행이 3행에 도달하면 인터프리터는 "f1"의 인스턴스를 생성하고 이를 ARI[big]에 저장한 다음 정적 링크를 ARI[big]에 연결합니다. 다음 줄. 인터프리터는 "f2" 인스턴스를 생성하고 정적 체인을 연결합니다. 그런 다음 5행에서 f2는 ARI[f1]을 생성하기 위해 호출됩니다. f2는 f1을 호출하여 ARI[f1]을 생성합니다. f1이 x를 출력하려면 x를 처리해야 합니다.
단순 변수 주소 지정
계속합니다. 이제 x를 주소 지정해야 하지만 x는 f1의 지역 변수에 나타나지 않으므로 인터프리터는 출력에서 ​​x를 찾기 위해 스택을 검색해야 합니다. 인터프리터는 "스택" 레이어를 따라 검색하지 않지만 현재 "스택"은 다음과 같기 때문에 점프가 있습니다.
|f1 | ←스레드 포인터
|f2 x = 2
| big | x = 1
|HOST|
인터프리터가 실제로 스택을 따라 계층별로 검색하면 출력은 2가 됩니다. 이는 Js 변수 주소 지정의 본질, 즉 정적 체인을 따라 검색하는 내용을 다룹니다.
위 문제를 이어가면 실행 포인터는 f1의 정적 체인을 따라 검색하여 big을 찾습니다. 그러면 big이 x=1이므로 1이 출력되고 모든 것이 정상입니다.
그렇다면 정적 링크가 루프를 형성하여 주소 지정의 "무한 루프"를 유발하게 됩니까? 걱정하지 마세요. 함수가 서로 중첩되어 있다는 사실을 기억하시나요? 즉, 함수는 루트 트리를 형성하고 모든 정적 체인 포인터는 결국 호스트에 집계되어야 합니다. 따라서 "포인터 루프"에 대해 걱정하는 것은 터무니없는 일입니다. (반대로 동적 범위 언어 주소 지정은 쉽게 무한 루프를 일으킬 수 있습니다.)
이제 간단한 변수 주소 지정 방법을 요약할 수 있습니다. 이제 인터프리터는 현재 함수의 지역 변수에서 변수 이름을 찾습니다. 찾을 수 없으면 따라옵니다. 정적 체인은 변수가 발견될 때까지 역추적하거나 호스트를 역추적하고 변수가 여전히 발견되지 않을 때까지 추적합니다.
ARI의 수명
이제 ARI를 살펴보겠습니다. ARI는 로컬 변수(매개변수 포함), 이 포인터, 동적 체인, 그리고 가장 중요한 것은 함수가 실행될 때 함수 인스턴스의 주소를 기록합니다. ARI의 구조는 다음과 같습니다.
ARI :: {
variables :: *variableTable, //Variable table
dynamicLink :: *ARI, //Dynamic link
instance :: * functioninst //함수 인스턴스
}
변수에는 모든 지역 변수, 매개변수 및 이 포인터가 포함됩니다. DynamicLink는 호출되는 ARI를 가리키고 함수 인스턴스를 가리킵니다. 함수 인스턴스에는 다음이 있습니다:
functioninst:: {
source:: *jsOperations, //함수 지침
staticLink:: *ARI, //Static link
......
}
함수가 호출되면 다음 "공식 코드"가 실제로 실행됩니다.
*ARI p
p = new ARI()
p->dynamicLink = thread. currentARI;
p->instance = 호출된 함수
p->variables.insert(매개변수 목록, 이 참조)
thread.transfer(p->instance->Operations[0])
Did 보이나요? ARI를 생성하고 매개변수와 이를 변수 테이블에 푸시한 다음 스레드 포인터를 함수 인스턴스의 첫 번째 명령어로 전송합니다.
함수 생성 시기는 어떻게 되나요? 함수 명령이 할당된 후에는 다음도 필요합니다.
newFunction->staticLink = thread.currentARI
이제 문제는 명확해졌습니다. 함수가 정의될 ​​때 현재 ARI를 직접 가리키는 정적 링크를 만들었습니다. 스레드의. 이것은 거의 모든 간단한 변수 주소 지정 문제를 설명할 수 있습니다. 예를 들어 다음 코드는 다음과 같습니다.
function test(){
for(i=0;i(function(t){ //이 익명 함수는 잠정적으로 f setTimeout(function(){echo('' t)},1000) //여기서 익명 함수는 g
})(i)
}
}
test()
This 이 코드의 효과는 1초 지연 후 0 1 2 3 4 순서로 출력되는 것입니다. setTimeout이 생성될 때 정적 링크는 ARI의 변수 테이블에 i를 포함합니다(매개변수는 로컬 변수로 간주됨). setTimeout이 만료되면 익명 함수 g는 익명 함수 f의 ARI에서 발견되는 변수 t를 검색합니다. 따라서 생성된 순서대로 0 1 2 3 4 가 하나씩 출력됩니다.
공개 익명 함수 f의 함수 인스턴스에는 총 5개의 ARI가 있습니다. 이에 따라 g도 5번 "생성"됩니다.첫 번째 setTimeout이 만료되기 전에 스택에는 다음과 같은 레코드가 있습니다(g를 5로 별도로 썼습니다).
ARI of test [i=5 at the end of the loop]
| ARI of f; t =0 ←——————g0의 정적 링크
| aRI of f; t=1 ←——————g1의 정적 링크
| t=2 —— ————g2의 정적 링크
| aRI of f; t=3 ←——————g3의 정적 링크
| f의 aRI ←————— g4 링크의 정적
------
그리고 g0이 호출되면 "스택"은 다음과 같습니다.
테스트의 ARI [루프 끝에서 i=5]
| f의 ARI; t=0 ←——————g0의 정적 링크
| f의 ARI;t=1 ←——————f의 정적 링크
| t=2 ←—— ————g2의 정적 링크
| f의 ARI; t=3 ←—————g3의 정적 링크
| t=4의 ARI; ————g4 정적 링크
------
g0의 ARI
| 여기서 t를 처리해야 하므로... t=0
------
g0의 ARI는 가능합니다. f 시리즈의 ARI에는 없으며 호스트에 직접 배치된 것으로 간주할 수 있습니다. 그러나 문제를 해결하는 정적 링크는 여전히 각 f의 ARI에 찔려 있습니다. 당연히 실수는 없을 것입니다. 왜냐하면 setTimeout이 대기 대기열에 순차적으로 푸시되기 때문입니다. 따라서 최종 출력은 0 1 2 3 4 순서입니다.
함수를 재정의하면 정적 링크도 수정되나요?
이제 다음 질문을 살펴보겠습니다. 함수가 정의되면 정적 링크가 설정됩니다. 그러면 함수가 재정의되면 또 다른 정적 링크가 설정됩니까? 먼저 예제를 살펴보겠습니다.
var x = "x in host";
f = function(){echo(x)}
f()
function big(); 🎜>var x = 'x in big';
f();
f = function(){echo (x)}
f()
}
big()
출력:
x in 호스트
x in 호스트
x in big
이 예는 대규모 실행 시 호스트의 f가 재정의되고 "new" f는 big을 가리키므로 마지막 줄은 'x in big'을 출력합니다.
그러나 다음 예는 훨씬 더 흥미롭습니다.
var x = "x in host";
f = function(){echo(x)};
f(); function big(){
var x = 'x in big';
var f1 = f
f
f; ()
}
big()
출력:
x in 호스트
x in 호스트
x in 호스트
x in 호스트
는 재정의가 정적 링크를 수정하시겠습니까? 그러나 여기서 두 할당은 단지 할당일 뿐이며 f1과 f의 포인터만 수정됩니다(JS 함수가 참조 유형이라는 것을 기억하세요?). f의 실제 인스턴스에서는 정적 링크가 변경되지 않았습니다! . 따라서 4개의 출력은 실제로 호스트의 x입니다.
구조(객체)에서 컴포넌트(속성)를 어드레싱하는 문제
기독교(java), 모르몬교(csh) 분들이 이상한 이름을 사용하는 점 양해 부탁드립니다만, JS 객체는 Hash와 너무 비슷합니다. 표에서 이 주소 지정 문제를 고려해 보겠습니다.
a.b 컴파일된 언어는 a를 찾은 다음 b를 찾기 위해 특정 거리만큼 뒤로 오프셋하는 코드를 생성합니다. 그러나 JS는 완전히 동적 언어이며 개체의 멤버는 다음을 수행할 수 있습니다. 프로토타입의 문제는 JS 객체 멤버의 주소 지정을 매우 흥미롭게 만듭니다.
객체는 해시 테이블입니다
몇 가지 특수 메서드(및 프로토타입 멤버)를 제외하면 메서드와 속성이 "해시 테이블"의 "격자"에 저장될 수 있기 때문에 객체는 해시 테이블과 거의 동일합니다. 안에. Yue 버전은 "JS Return of the King"에서 HashTable 클래스를 구현했습니다.
객체 자체의 속성 처리
"소유" 속성은 hasOwnProperty가 true인 속성을 나타냅니다. 구현 관점에서 볼 때 이는 개체 자체의 "해시 테이블"의 구성원입니다. 예:
function Point(x,y){
this.x = x;
this.y = y
}
var a = new Point(1,2);
echo("a.x:" a.x)
Point 생성자는 "Point" 객체 a를 생성하고 x 및 y 속성을 설정합니다. 따라서 a의 멤버 테이블에는 다음이 있습니다.
| - --> 1
| y | ---> 2
a.x를 검색할 때 인터프리터는 먼저 a를 찾은 다음 a의 멤버 테이블에서 x를 검색하여 1을 얻습니다.
생성자에서 개체에 메서드를 설정하는 것은 동일한 유형의 두 개체가 서로 다른 메서드를 갖게 되므로 좋은 전략이 아닙니다.
function Point(x,y){
this.x = x;
this.y = y;
this.abs = function(){return Math.sqrt(this.x*this.x this.y*this.y)}
}
var a = new Point (1,2);
var b = new Point(1,2);
echo("a.abs == b.abs ? " (a.abs==b.abs)) ;
echo("a.abs === b.abs ? " (a.abs===b.abs))
두 출력 모두 false입니다. 네 번째 줄에서 객체의 abs 멤버( method )가 매번 생성되므로 a.abs와 b.abs는 실제로 완전히 다른 두 함수 인스턴스를 가리킵니다. 따라서 동일해 보이는 두 메서드는 실제로 동일하지 않습니다.
프로토타입 주소 지정 문제 제기
프로토타입은 함수(클래스)의 속성으로, 객체(클래스가 아님)를 가리킵니다. "프로토타입"이라는 개념은 "고양이에서 호랑이를 그리는 것"에 비유할 수 있습니다. "호랑이"와 다른 것을 물려받은 "고양이" 사이에는 관계가 없으며 "호랑이"와 "고양이" 사이의 관계만 있을 뿐입니다. ". 프로토타입은 유사성에 중점을 둡니다. js에서 코드는 다음과 같이 작성할 수 있습니다.
Tiger.prototype = new Cat() 함수의 프로토타입은 빈 객체일 수도 있습니다.
SomeClass.prototype = {} 로 돌아가서 특정 속성을 얻기 위해 .을 사용했는데 해당 속성이 프로토타입에 있는 경우 어떻게 될까요? 현상은: 실제로 얻은 것인데 어떻게 얻었습니까? 객체 자체의 속성 이름이 프로토타입 속성의 이름과 같으면 어떻게 되나요? 다행스럽게도 개체 자체의 속성이 우선적으로 적용됩니다.
プロトタイプでメソッドを定義することは、優れた設計戦略です。上記の例を変更すると、次のようになります。
function Point(x,y){
this.x = x;
this.y = y;
Point.prototype.abs = function (){return Math.sqrt(this.x*this.x this.y*this,y)}
var a = 新しい点(1,2)
var b = 新しい点(1, 2); );
echo("a.abs == b.abs ? " (a.abs==b.abs));
echo("a.abs === b.abs ? " (a . abs===b.abs));
これで、最終的に出力が等しくなります。その理由は、a.abs と b.abs が Point クラス プロトタイプのメンバー abs を指しているためです。ただし、Point.prototype.abs に直接アクセスすることはできず、テスト中にエラーが発生します。訂正: 再テストしたところ、「Point.prototype.abs にアクセスできない」問題は、使用した JSConsole の問題であることがわかりました。返信は正しいです、修正してくれてありがとう!
プロトタイプのチェーンは非常に長くなったり、ループ状に巻かれたりする場合もあります。次のコードを考えてみましょう:
A = function(x){this.x = x};
B = function(x){this.y = x};
A.prototype = new B(1) );
B.prototype = new A(1);
var a = new A(2)
var b = new B(2); ;
echo(b.x ' , ' b.y);
ここで説明されている関係は、おそらく「私はあなたと同じであり、あなたも私と同じです」です。プロトタイプ ポインターにより、次の出力が発生します。
2, 1
1, 2
a.y を検索すると、プロトタイプ チェーンに沿って「a.prototype」が見つかり、同じ原則が適用されます。 b.x に。ここで、未登録の属性「a.z」を出力したいとします:
echo(tyoeof a.z) ここに無限ループがないことに驚きます。インタプリタには、プロトタイプチェーンがなる問題に対処するメカニズムがあるようです。ループ。同時に、プロトタイプはツリーまたは単一のリングを形成し、複数のリング構造を持ちません。これは非常に単純なグラフ理論です。
これ: 関数の隠しルール
メソッド (関数) 呼び出しで最も厄介な隠しルールはこの問題です。論理的に言えば、これは呼び出し元 (オブジェクト) を指すポインターです。しかし、これが常に発信者を指していれば、世界は素晴らしいでしょう。しかし、このいまいましいポインターは時々「犬を蹴る」ことがあります。可能な変更には、呼び出し、適用、非同期呼び出し、および「window.eval」が含まれます。
私は、lua の self と同じように、これをパラメータとして扱うことを好みます。 Lua の self は明示的に渡すことも、コロンを使用して呼び出すこともできます:
a:f(x,y,z) === a.f(a,x,y,z) JS の「プライム」メソッド呼び出しにも同じことが当てはまります。これは次のようになります:
a.f(x,y,z) === a.f.call(a,x,y,z)f.call は、 Lua のクリーンな呼び出しと同じように、真に「クリーンな」呼び出し形式です。 Lua は JS の明確なバージョンであると多くの人が言いますが、これは真実です。
「これ」を修正する原則
「王の帰還」で前述した「クロージャを使用してこれを修正する」の最初のコードを見てください:
button1.onclick = (
function( e){return function (){button_click.apply(e,arguments)}}
)(button1) このコード行を過小評価しないでください。実際、このコードは ARI を作成し、ここで button1 をバインドしてから、この関数は呼び出し元 (件名) に対して e を強制的に呼び出すため、button_click に渡されるのは e、つまり button1! です。イベント バインディングが完了すると、環境は次のようになります。
button1.onclick = _F_; //返された匿名関数の名前を設定します。
_F_.staticLink = _ARI_; //後に呼び出される匿名関数Creation ARI
_ARI_[e] = button1 //匿名 ARI パラメータ テーブルの e は、_F_ が探している e でもあります
したがって、ボタンをクリックすると、_F_ が呼び出され、_F_ が One を開始しますcaller は e の button_click 関数です。以前の分析によれば、e は button1 と等しいため、安全な「指定された呼び出し元」メソッドが得られます。おそらく、このアイデアを開発し続けて、ユニバーサル インターフェイスを作成できるでしょう。
bindFunction = function(f,e){ //私たちは善良な人間なので、プロトタイプを変更したり、変更したりしません...
return function(){
f.apply(e,arguments)
}
}

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.