JavaScript의 범위와 컨텍스트에 대한 깊은 이해(그림)

黄舟
풀어 주다: 2017-03-09 14:14:55
원래의
1477명이 탐색했습니다.

JavaScript의 범위 및 컨텍스트 구현은 부분적으로 고유한 유연성으로 인해 이 언어의 매우 독특한 기능입니다. 함수는 다양한 컨텍스트와 범위를 허용할 수 있습니다. 이러한 개념은 JavaScript의 수많은 강력한 디자인 패턴을 위한 견고한 기반을 제공합니다. 그러나 이 개념은 개발자에게 쉽게 혼란을 야기할 수도 있습니다. 이를 위해 이 글에서는 이러한 개념을 종합적으로 분석하고 다양한 디자인 패턴이 이를 어떻게 활용하는지 설명할 것입니다.

컨텍스트와 범위

먼저 알아야 할 것은 컨텍스트와 범위는 완전히 다른 개념이라는 것입니다. 수년에 걸쳐 나는 많은 개발자(나를 포함)가 이 두 개념을 혼동하고 실수로 두 개념을 혼합한다는 것을 발견했습니다. 공평하게 말하면, 지난 몇 년 동안 많은 용어가 혼란스럽게 사용되었습니다.

모든 함수 호출에는 함수와 밀접하게 연관된 범위와 컨텍스트가 있습니다. 기본적으로 범위는 기능 기반인 반면 컨텍스트는 개체 기반입니다. 즉, 범위에는 호출된 함수의 변수 액세스가 포함되며 호출 시나리오에 따라 다릅니다. 컨텍스트는 항상 현재 실행 중인 코드를 소유(제어)하는 객체에 대한 참조인 this 키워드의 값입니다.

변수 범위

변수는 로컬 또는 전역 범위에서 정의할 수 있으며, 이는 런타임 중 변수의 접근성에 대한 다양한 범위를 설정합니다. 정의된 모든 전역 변수는 함수 본문 외부에서 선언되어야 하고 전체 런타임 동안 유지되며 모든 범위에서 액세스할 수 있음을 의미합니다. ES6 이전에는 지역 변수가 함수 본문 내에서만 존재할 수 있었고 함수가 호출될 때마다 범위가 달랐습니다. 지역 변수는 호출된 시간 범위 내에서만 할당, 검색 및 조작할 수 있습니다.

ES6 이전에는 JavaScript가 블록 수준 범위를 지원하지 않았다는 점에 유의해야 합니다. 즉, if 문, switch 문, for 루프 및에서는 블록 수준 범위를 지원할 수 없습니다. while 루프 도메인. 즉, ES6 이전의 JavaScript는 Java와 유사한 블록 수준 범위를 구축할 수 없습니다(변수는 명령문 블록 외부에서 액세스할 수 없습니다). 하지만 ES6부터는 let 키워드를 통해 변수를 정의할 수 있는데, 이는 var 키워드의 단점을 보완하고, 자바 언어와 같은 변수 정의가 가능하며, 블록 수준의 범위를 지원한다. 두 가지 예를 살펴보십시오.

ES6 이전에는 var 키워드를 사용하여 변수를 정의했습니다.

function func() {
  if (true) {
    var tmp = 123;
  }
  console.log(tmp); // 123
}
로그인 후 복사

var 키워드로 선언된 변수에 변수 승격 프로세스가 있으므로 액세스할 수 있습니다. ES6 시나리오에서는 변수를 정의하기 위해 let

function func() {
  if (true) {
    let tmp = 123;
  }
  console.log(tmp); // ReferenceError: tmp is not defined
}
로그인 후 복사

키워드를 사용하는 것이 좋습니다. 이 방법을 사용하면 많은 오류를 피할 수 있습니다.

컨텍스트this

컨텍스트는 일반적으로 함수가 호출되는 방식에 따라 다릅니다. 함수가 개체의 메서드로 호출되면

이 메서드가 호출되는 개체로 설정됩니다. this

var obj = {
    foo: function(){
        alert(this === obj);    
    }
};

obj.foo(); // true
로그인 후 복사

이 지침은

연산자를 사용하여 함수를 호출할 때도 적용됩니다. 객체의 인스턴스. 이 경우 함수 범위 내의 new 값은 새로 생성된 인스턴스로 설정됩니다. this

function foo(){
    alert(this);
}

new foo() // foo
foo() // window
로그인 후 복사

바인딩된 함수가 호출되면

는 기본적으로 전역 컨텍스트이며 브라우저에서는 this 객체를 가리킵니다. ES5에는 엄격 모드 개념이 도입되었습니다. 엄격 모드가 활성화되면 컨텍스트의 기본값은 window입니다. undefined

실행 컨텍스트

JavaScript는 단일 스레드 언어이므로 동시에 하나의 작업만 실행할 수 있습니다. JavaScript 인터프리터가 코드 실행을 위해 초기화되면 먼저 전역 실행 컨텍스트(실행 컨텍스트)가 기본값으로 설정됩니다. 이 시점부터 함수를 호출할 때마다 새로운 실행 컨텍스트가 생성됩니다.

이것은 종종 초보자에게 혼란을 야기합니다. 여기서는 변수나 함수가 액세스할 수 있는 다른 데이터를 정의하고 해당 동작을 결정하는 실행 컨텍스트라는 새로운 용어가 언급됩니다. 앞서 논의한 컨텍스트보다는 범위의 역할에 더 편향되어 있습니다. 실행 환경과 컨텍스트라는 두 가지 개념을 주의 깊게 구분하시기 바랍니다. (참고: 영어는 쉽게 혼동을 일으킬 수 있습니다.) 솔직히 말해서 이것은 매우 나쁜 명명 규칙이지만 ECMAScript 사양에 의해 설정되었으므로 이를 따르는 것이 좋습니다.

각 기능에는 고유한 실행 환경이 있습니다. 실행 흐름이 함수에 들어가면 함수의 환경이 환경 스택(실행 스택)으로 푸시됩니다. 함수가 실행된 후 스택은 해당 환경을 팝하고 이전 실행 환경으로 제어를 반환합니다. ECMAScript 프로그램의 실행 흐름은 이 편리한 메커니즘으로 제어됩니다.

执行环境可以分为创建和执行两个阶段。在创建阶段,解析器首先会创建一个变量对象(variable object,也称为活动对象 activation object), 它由定义在执行环境中的变量、函数声明、和参数组成。在这个阶段,作用域链会被初始化,this的值也会被最终确定。 在执行阶段,代码被解释执行。

每个执行环境都有一个与之关联的变量对象(variable object),环境中定义的所有变量和函数都保存在这个对象中。 需要知道,我们无法手动访问这个对象,只有解析器才能访问它。

作用域链(The Scope Chain)

当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。作用域链的用途是保证对执行环境有权访问的所有变量和函数的有序访问。 作用域链包含了在环境栈中的每个执行环境对应的变量对象。通过作用域链,可以决定变量的访问和标识符的解析。 注意,全局执行环境的变量对象始终都是作用域链的最后一个对象。我们来看一个例子:

var color = "blue";

function changeColor(){
  var anotherColor = "red";

  function swapColors(){
    var tempColor = anotherColor;
    anotherColor = color;
    color = tempColor;

    // 这里可以访问color, anotherColor, 和 tempColor
  }

  // 这里可以访问color 和 anotherColor,但是不能访问 tempColor
  swapColors();
}

changeColor();

// 这里只能访问color
console.log("Color is now " + color);
로그인 후 복사

上述代码一共包括三个执行环境:全局环境、changeColor()的局部环境、swapColors()的局部环境。 上述程序的作用域链如下图所示:

scope chain example

从上图发现。内部环境可以通过作用域链访问所有的外部环境,但是外部环境不能访问内部环境中的任何变量和函数。 这些环境之间的联系是线性的、有次序的。

对于标识符解析(变量名或函数名搜索)是沿着作用域链一级一级地搜索标识符的过程。搜索过程始终从作用域链的前端开始, 然后逐级地向后(全局执行环境)回溯,直到找到标识符为止。

闭包

闭包是指有权访问另一函数作用域中的变量的函数。换句话说,在函数内定义一个嵌套的函数时,就构成了一个闭包, 它允许嵌套函数访问外层函数的变量。通过返回嵌套函数,允许你维护对外部函数中局部变量、参数、和内函数声明的访问。 这种封装允许你在外部作用域中隐藏和保护执行环境,并且暴露公共接口,进而通过公共接口执行进一步的操作。可以看个简单的例子:

function foo(){
    var localVariable = 'private variable';
    return function bar(){
        return localVariable;
    }
}

var getLocalVariable = foo();
getLocalVariable() // private variable
로그인 후 복사

模块模式最流行的闭包类型之一,它允许你模拟公共的、私有的、和特权成员:

var Module = (function(){
    var privateProperty = 'foo';

    function privateMethod(args){
        // do something
    }

    return {

        publicProperty: '',

        publicMethod: function(args){
            // do something
        },

        privilegedMethod: function(args){
            return privateMethod(args);
        }
    };
})();
로그인 후 복사

模块类似于一个单例对象。由于在上面的代码中我们利用了(function() { ... })();的匿名函数形式,因此当编译器解析它的时候会立即执行。 在闭包的执行上下文的外部唯一可以访问的对象是位于返回对象中的公共方法和属性。然而,因为执行上下文被保存的缘故, 所有的私有属性和方法将一直存在于应用的整个生命周期,这意味着我们只有通过公共方法才可以与它们交互。

另一种类型的闭包被称为立即执行的函数表达式(IIFE)。其实它很简单,只不过是一个在全局环境中自执行的匿名函数而已:

(function(window){

    var foo, bar;

    function private(){
        // do something
    }

    window.Module = {

        public: function(){
            // do something 
        }
    };

})(this);
로그인 후 복사

对于保护全局命名空间免受变量污染而言,这种表达式非常有用,它通过构建函数作用域的形式将变量与全局命名空间隔离, 并通过闭包的形式让它们存在于整个运行时(runtime)。在很多的应用和框架中,这种封装源代码的方式用处非常的流行, 通常都是通过暴露一个单一的全局接口的方式与外部进行交互。

Call和Apply

这两个方法内建在所有的函数中(它们是Function对象的原型方法),允许你在自定义上下文中执行函数。 不同点在于,call函数需要参数列表,而apply函数需要你提供一个参数数组。如下:

var o = {};

function f(a, b) {
  return a + b;
}

// 将函数f作为o的方法,实际上就是重新设置函数f的上下文
f.call(o, 1, 2);    // 3
f.apply(o, [1, 2]); // 3
로그인 후 복사

两个结果是相同的,函数f在对象o的上下文中被调用,并提供了两个相同的参数12

在ES5中引入了Function.prototype.bind方法,用于控制函数的执行上下文,它会返回一个新的函数, 并且这个新函数会被永久的绑定到bind方法的第一个参数所指定的对象上,无论该函数被如何使用。 它通过闭包将函数引导到正确的上下文中。对于低版本浏览器,我们可以简单的对它进行实现如下(polyfill):

if(!('bind' in Function.prototype)){
    Function.prototype.bind = function(){
        var fn = this, 
            context = arguments[0], 
            args = Array.prototype.slice.call(arguments, 1);
        return function(){
            return fn.apply(context, args.concat(arguments));
        }
    }
}
로그인 후 복사

bind()方法通常被用在上下文丢失的场景下,例如面向对象和事件处理。之所以要这么做, 是因为节点的addEventListener方法总是为事件处理器所绑定的节点的上下文中执行回调函数, 这就是它应该表现的那样。但是,如果你想要使用高级的面向对象技术,或需要你的回调函数成为某个方法的实例, 你将需要手动调整上下文。这就是bind方法所带来的便利之处:

function MyClass(){
    this.element = document.createElement('p');
    this.element.addEventListener('click', this.onClick.bind(this), false);
}

MyClass.prototype.onClick = function(e){
    // do something
};
로그인 후 복사

回顾上面bind方法的源代码,你可能会注意到有两次调用涉及到了Arrayslice方法:

Array.prototype.slice.call(arguments, 1);
[].slice.call(arguments);
로그인 후 복사

我们知道,arguments对象并不是一个真正的数组,而是一个类数组对象,虽然具有length属性,并且值也能够被索引, 但是它们不支持原生的数组方法,例如slicepush。但是,由于它们具有和数组类似的行为,数组的方法能够被调用和劫持, 因此我们可以通过类似于上面代码的方式达到这个目的,其核心是利用call方法。

这种调用其他对象方法的技术也可以被应用到面向对象中,我们可以在JavaScript中模拟经典的继承方式:

MyClass.prototype.init = function(){
    // call the superclass init method in the context of the "MyClass" instance
    MySuperClass.prototype.init.apply(this, arguments);
}
로그인 후 복사

也就是利用callapply在子类(MyClass)的实例中调用超类(MySuperClass)的方法。

ES6中的箭头函数

ES6中的箭头函数可以作为Function.prototype.bind()的替代品。和普通函数不同,箭头函数没有它自己的this值, 它的this值继承自外围作用域。

对于普通函数而言,它总会自动接收一个this值,this的指向取决于它调用的方式。我们来看一个例子:

var obj = {

  // ...

  addAll: function (pieces) {
    var self = this;
    _.each(pieces, function (piece) {
      self.add(piece);
    });
  },

  // ...

}
로그인 후 복사

在上面的例子中,最直接的想法是直接使用this.add(piece),但不幸的是,在JavaScript中你不能这么做, 因为each的回调函数并未从外层继承this值。在该回调函数中,this的值为windowundefined, 因此,我们使用临时变量self来将外部的this值导入内部。我们还有两种方法解决这个问题:

使用ES5中的bind()方法

var obj = {

  // ...

  addAll: function (pieces) {
    _.each(pieces, function (piece) {
      this.add(piece);
    }.bind(this));
  },

  // ...

}
로그인 후 복사

使用ES6中的箭头函数

var obj = {

  // ...

  addAll: function (pieces) {
    _.each(pieces, piece => this.add(piece));
  },

  // ...

}
로그인 후 복사

在ES6版本中,addAll方法从它的调用者处获得了this值,内部函数是一个箭头函数,所以它集成了外部作用域的this值。

注意:对回调函数而言,在浏览器中,回调函数中的thiswindowundefined(严格模式),而在Node.js中, 回调函数的thisglobal。实例代码如下:

function hello(a, callback) {
  callback(a);
}

hello('weiwei', function(a) {
  console.log(this === global); // true
  console.log(a); // weiwei
});
로그인 후 복사

小结

在你学习高级的设计模式之前,理解这些概念非常的重要,因为作用域和上下文在现代JavaScript中扮演着的最基本的角色。 无论我们谈论的是闭包、面向对象、继承、或者是各种原生实现,上下文和作用域都在其中扮演着至关重要的角色。 如果你的目标是精通JavaScript语言,并且深入的理解它的各个组成,那么作用域和上下文便是你的起点。


위 내용은 JavaScript의 범위와 컨텍스트에 대한 깊은 이해(그림)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

원천:php.cn
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
최신 이슈
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿
회사 소개 부인 성명 Sitemap
PHP 중국어 웹사이트:공공복지 온라인 PHP 교육,PHP 학습자의 빠른 성장을 도와주세요!