带你走进JS做用域(Scope)的世界

该文章是直接翻译国外一篇文章,关于做用域(Scope)。
都是基于原文处理的,其余的都是直接进行翻译可能有些生硬,因此为了行文方便,就作了一些简单的本地化处理。
同时也新增了本身的理解和对应的思考过程,若有不对,请在评论区指出
若是想直接根据原文学习,能够忽略此文。javascript

若是你以为能够,请多点赞,鼓励我写出更精彩的文章🙏。
若是你感受有问题,也欢迎在评论区评论,三人行,必有我师焉
前端

TL;DR

  • 概念介绍
  • 最少访问原则
  • JS中的做用域
  • 全局做用域
  • 局部做用域
  • 块级声明
  • 上下文(Context)
  • 执行上下文(Execution Context)
  • 词法做用域(Lexical Scope)
  • 闭包
  • 公共做用域和私有做用域
  • 经过.call(), .apply() and .bind()修改上下文

概念介绍

在JS中有一个比较特殊的特性:做用域(Scope)。尽管做用域这个概念对于许多新手不是很容易能掌握,可是咱们经过一些简单的示例来,来解释这些概念。java

Scope可以让你的代码在运行的时候,有权访问一些变量函数对象。换句话说,做用域决定:你所写代码中可以访问哪些变量或者其余资源数据。编程

最少访问原则

在你的代码中如何才能够实现让变量“私有化”,换句话说,如何让变量成为受限的,而不是随处可见的,这是Scope所关注的点。经过Scope咱们可让代码变的更加安全。而在电脑安全策略中一个很重要的原则就是只有拥有对应权限的用户才能够访问对应变量。而这个原则一样适用于编程。在大多数编程语言中,咱们称其为做用域设计模式

JS中的做用域

在JS中存在两类做用域:数组

  • 全局做用域
  • 局部做用域

在函数中定义的变量称为局部做用域,在函数外包定义的变量称为全局做用域。函数在调用时,会新建一个做用域。浏览器

全局做用域

当你在一个文档中开始写代码的时候,其实已经位于全局做用域范围以内了。而贯穿整个JS文档,有且只有一个全局做用域。若是一个变量定义在函数体以外,那这个变量确定是一个全局做用域。安全

var name = '北宸';
复制代码

定义在全局做用域中的变量能够在其余做用域中被获取或者修改。闭包

var name = '北宸';

console.log(name); // '北宸'

function logName() {
    console.log(name); // 'name' 可以被获取也能够被修改
}

logName(); // '北宸'
复制代码

局部做用域

在函数中定义的变量是属于局部做用域的。而且,每次调用函数生成的局部做用域也是不同的。也就是说,相同名字的变量能够出如今不一样的函数中。这是由于,这些变量是和他们本身的做用域挂钩的,每个函数被调用,都生成新的局部做用域,而且对其余函数的做用域没有访问权限。app

// 全局做用域
function someFunction() {
    // 局部做用域#1
    function someOtherFunction() {
        // 局部做用域#2
    }
}

// 全局做用域
function anotherFunction() {
    // 局部做用域#3
}
// 全局做用域
复制代码

块级声明

ifswitch或者是for循环、while被称为块级声明。他们和函数不一样,经过他们包裹的代码,不会生成新的做用域。在块级声明中定义的变量仍是属于块级声明所在做用域范围以内。

if (true) {
    // 不会新增做用域
    var name = '北宸'; // name 仍是属于全局做用域
}

console.log(name); // '北宸'
复制代码

ECMAScript 6新添了letconst关键字。而该关键字能够替代var的使用。

var name = '北宸';

let likes = '南蓁';
const skills = '我是一个萌萌哒的汉子';
复制代码

var关键字不一样的是:letconst关键字能够在块级声明中定义变量,从而生成一个新的局部做用域

if (true) {
    
    // if语句,不会生成做用域t

    // 经过var定义的变量属于全局做用域范围
    var name = '北宸';
    // 局部做用域
    let likes = '南蓁';
    // 局部做用域
    const skills = '我是一个萌萌哒的汉子';
}

console.log(name); // '北宸'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defin
复制代码

让咱们分析一波,咱们经过断点跟踪,发现经过letconst在块级声明中定义的变量,它的存放方式不用var定义的变量的存放方式是不同的。

name是挂载在 Window上也就是咱们说的全局做用域( Global)。

全局做用域的生存时间是贯穿整个项目,而局部做用域是伴随函数的生命周期存在而存在。

上下文(Context)

许多开发者对做用域(Scope)和上下文(Context)不是很容易区分,老是认为他们是一个东西。可是实际上他们是不同的。做用域是咱们上面讨论的那样,说的直白一点,就是数据权限问题。而上下文指代this的值。

Scope侧重于变量的可见性,而Context指向this的值。咱们能够经过一些方法来修改上下文。而在全局做用域中,上下文恒等于Window(在浏览器环境)

// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
console.log(this);

function logFunction() {
    console.log(this);
}
// Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// js中this的指向是在调用时候,肯定的
logFunction(); 

复制代码

若是函数定义在对象中,而且在对象做用域范围中调用,this就指向对象

class User {
    logName() {
        console.log(this);
    }
}

(new User).logName(); // User {}
复制代码

Notice:若是经过new关键字来调用函数,context将会被赋值为函数的实例。

function logFunction() {
    console.log(this);
}

new logFunction(); // logFunction {}
复制代码

在严格模式Strict Mode下调用函数,context的值为undefined

执行上下文(Execution Context)

为了避免被上文中所讲的所影响,须要特别指出执行上下文(Execution Context)中的context指向的是做用域(Scope),而不是上下文(context)。这是一个让人很容易误会的名字,可是这是JS的规范或者是语法命名,咱们只能去适应他。

JS是一个单线程的语言,因此在同一时刻,只能执行一个任务。而剩余的任务,就会在执行环境(Execution Context)中以队列的形式等待。正如上文说的,当JS编译器开始处理代码的时候,上下文(做用域)默认为全局做用域。该全局上下文被填充执行上下文(Execution Context)中,做为第一个上下文,随后开始执行对应代码。

随后,每当函数被调用,新建的上下文将会被以队列的形式追加到执行上下文中(这个追加过程是在函数调用的瞬间完成的)。另一个函数调用,也会周而复始的执行如上追加操做。

每一个函数建立属于本身的执行上下文。

当JS编译器处理完某个上下文中的代码时,该上下文将会被执行上下文移除,同时执行上下文中的current context指向被移除上下文的父级上下文。JS编译器老是处理位于执行堆栈顶层(这其实是代码中做用域的最内层 )的上下文。

全局上下文只有一个,而函数上下文能够存在多个

执行上下文存在两个阶段:构建执行

构建阶段

构建阶段是处于函数被调用可是还未执行的时候。将依次发生以下过程:

  • 生成变量(活动)对象
  • 构建做用域链
  • 为上下文(this)赋值

变量(活动)对象

变量对象也被称为活动对象,它包含很全部在执行上下文的特定分支中定义的变量,函数还有其余的声明。当函数被调用,JS编译器就会扫描函数中的全部资源,包括函数参数,变量还有其余声明。而后将全部的资源打包到一个对象中,这个对象就是变量对象

'variableObject': {
    // 包含 函数参数, 内部变量 和函数声明
}

复制代码

做用域链

在执行上下文的构建阶段,做用域链的建立在变量对象以后。做用域链包含变量对象。做用域链用于查找和定位变量。当代码中须要某个变量时,JS老是从代码的最内层开始查找,若是在当前做用域中没有找到,就继续向父级做用域查找,周而复始,直到查找定位到变量定义的做用域。经过查找变量的机制可知,做用域链能够简单定义为一个对象,在对象内包含了表明其执行上下文的变量对象,还有该执行上下文的父级变量对象。而父级变量对象又能够包含父级的父级的变量对象,直到某一级的父级变量对象为null时中止。这里涉及到做用域链的查找机制,从另一个角度分析,做用域链是查找顶级变量对象,而JS中对象最后的归宿都是null。这一点能够参考理解JS中的原型(Prototypes)这篇文章。(也算是从另一个角度来考虑做用域链)

'scopeChain': {
    // 包含它本身的变量对象和表明其父级执行上下文的变量对象
}
复制代码

执行上下文对象

执行上下文能够简单的经过以下对象进行抽象表示:

executionContextObject = {
    'scopeChain': {}, //包含其自身的变量对象和其父级的执行上下文的变量对象 
    'variableObject': {}, // 包含函数参数,内部变量,还有方法定义
    'this': valueOfThis
}
复制代码

代码执行阶段

在执行上下文的第二阶段为代码执行阶段,其余的值被赋值,还有代码最终执行

词法做用域(Lexical Scope)

词法做用域说的是,在一个函数的做用域中定义一个子函数,该子函数拥有对外层函数变量和其余资源的访问权限。也就是说,子函数在词法上是与外层函数的执行上下文耦合的。词法做用域有时候也被称为静态做用域(Static Scope)

function grandfather() {
    var name = 'Hammad';
    // likes 在此处不能被访问
    function parent() {
        // name 在此处能被访问
        // likes 在此处不能被访问
        function child() {
            //做用域链的最内层 
            // name 在此处能被访问
            var likes = 'Coding';
        }
    }
}
复制代码

Notice:看上面的例子咱们发现,词法做用域是向前可见的,也就说在父级中定义的变量可以在子级执行上下文中访问。例如,name。可是,词法做用域是向后不可见的,子级定义的变量不能够在父级执行上下文中访问。

也演变出一个变量查找规则,若是在不一样的执行上下文存在相同的变量,而JS引擎在定位变量是按着由上到下的顺序遍历执行堆栈。在执行堆栈中存在多个同名变量,在最内层函数中(执行堆栈最顶层)拥有最高的访问权限。(在最内层执行上下文中访问变量)

闭包(Closures)

闭包的概念相似于词法做用域。当一个内层函数尝试访问外层函数的做用域链时,闭包产生了。这意味着变量位于词法做用域以外。闭包能访问内层函数本身的做用域链,它父级的做用域链和全局做用域。

闭包不只能访问在外层函数定义的变量,并且能够访问外层函数的参数列表。

闭包也能够在外层函数被执行以后继续访问外层函数的变量。也意味着,被返回的函数拥有访问外层函数的全部资源。

当一个函数调用时,返回了一个内层函数,返回的内层函数不会立马被调用。你必须用一个变量去接收被返回的内层函数的引用。而且将接收返回值的变量做为函数去调用。

function greet() {
    name = '北宸';
    return function () {
        console.log('Hi ' + name);
    }
}

greet(); // 只是一个简单的函数调用

// 返回的内层函数被greetLetter接收
greetLetter = greet();

 // 用调用函数的方式来处理变量
greetLetter(); // 'Hi 北宸'

复制代码

咱们也可不用经过变量来接收返回的内层函数,直接利用()()调用:

function greet() {
    name = '北宸';
    return function () {
        console.log('Hi ' + name);
    }
}

greet()(); // 'Hi 北宸'

复制代码

若是想详细了解,如何构建和使用闭包,能够参考JS闭包(Closures)了解一下

公共做用域和私有做用域

在许多其余编程语言中,你能够在class中设置属性和方法为publicprivate或者是protected的做用域。以下是PHP的使用方式

// 公共做用域
public $property;
public function method() {
  // ...
}

// 私有做用域
private $property;
private function method() {
  // ...
}

//受保护做用域
protected $property;
protected function method() {
  // ...
}
复制代码

经过对函数使用方式进行限定,使得免受一些没必要要的访问。可是在JS中,没有像publicprivate的关键词去限制变量。可是咱们能够经过闭包模拟这种特性。为了可以将咱们的代码与全局做用域分离,咱们须要封装以下的函数格式:

(function () {
  // 私有做用域
})();
复制代码

函数后面的括号表示,在函数定义的完成就告诉JS编译器当即调用该函数。咱们能够在函数中定义一些在外部没法获取的变量和方法。若是咱们想让其中定义的变量,部分对外部可见,咱们能够经过闭包实现。而封装数据的方式也被称为Module Pattern(设计模式中的一种)。

模块模式(Module Pattern)

var Module = (function() {
    function privateMethod() {
        // 私有
    }

    return {
        publicMethod: function() {
            // 此处对私有变量拥有访问权限
        }
    };
})();
复制代码

模块模式经过利用闭包,返回了一个匿名对象,在该对象中就是对应模块能被外界访问的公共(public)属性,而公共属性或者方法具备对模块中私有属性的访问权限。

在项目开发或者模块定义中,咱们能够将私有属性用_开头的变量与公共属性作区分。

var Module = (function () {
    function _privateMethod() {
        // do something
    }
    function publicMethod() {
        // do something
    }
    return {
        publicMethod: publicMethod,
    }
})();
复制代码

若是想对模块模式了解更多,或者了解JS的模块发展历程,能够参考骚年,你对前端模块化了解多少

经过.call(), .apply() and .bind()修改上下文

CallApply函数用于改变调用函数的上下文。

function hello() {
    // do something...
}

hello(); // 正常调用
hello.call(context); // 将context做为第一个参数传入
hello.apply(context); // 将context做为第一个参数传入
复制代码

.call().apply()之间的区别是,call接收的是以逗号分隔的参数list,而apply接收的是数组。

function introduce(name, interest) {
    console.log('Hi! 我是'+ name +' ,我喜欢'+ interest +'.');
    console.log('this' +'值为'+this)
}

introduce('北宸', 'Coding'); //
introduce.call(window, 'Batman', 'to save Gotham'); // 在context以后,以逗号分隔传递
introduce.apply('Hi', ['Bruce Wayne', 'businesses']); //在context以后,传入数组

复制代码

call的运行速度比apply

在JS中一切皆对象,做为函数也不例外,而JavaScript函数带有四个内置方法,它们是:

  • Function.prototype.apply()
  • Function.prototype.bind()(在ECMAScript 5(ES5)中引入)
  • Function.prototype.call()
  • Function.prototype.toString()

Function.prototype.toString()返回该函数的源代码的字符串表示形式。

咱们已经讨论过.call().apply()可以改变context。与Call 和 Apply不一样,Bind自己不会调用该函数,它只能用于在调用该函数以前绑定上下文值和其余参数。

(function introduce(name, interest) {
  console.log('Hi! 我是'+ name +' ,我喜欢'+ interest +'.');
    console.log('this' +'值为'+this)
}).bind(window, '北宸', '南蓁')();
复制代码

若是想对applybindcall有一个更深的了解能够参考this、apply、call、bind

相关文章
相关标签/搜索