JavaScript的做用域、闭包、(apply, call, bind)

介绍

JavaScript 有一个特征————做用域。理解做用域scope可使你的代码脱颖而出,减小错误,帮助你用它构造强大的设计模式。html

什么是做用域

做用域就是在代码执行期间变量,函数和对象能被获取到的特定的代码范围。换句话说,做用域决定了变量和其余资源在你的代码区域中的可见性。ajax

为何会有做用域?———最小存取原则

那么,限制变量的可见性不让其在代码中到处可见的意义是什么?优点之一 是做用域使你的代码具有必定的安全性。一个通用的计算机安全性原则就是让用户每次只访问他们须要的东西。编程

想一想计算机管理员:他们须要控制不少公司系统的东西,给他们彻底的用户权限彷佛是能够理解的。假设一个公司有三个系统管理员,他们都有系统的全部权限,一切进展顺利。可是忽然厄运降临,其中一人的系统被可恶的病毒感染了,而如今不知道是谁哪里出错了。如今意识到应该给他们基本权限的用户帐户只在他们须要的时候授予他们彻底的权限。这会帮助你追踪变更并一直知晓哪一个帐户作了什么。这就叫作最小存取原则。好像很直观吧,这个原则也用于程序语言设计,在包括JS在内的编程语言中它叫作做用域设计模式

当你享受编程之旅时,你会意识到你的代码的做用域部分帮助你提高效率,追踪bug并减小bug。做用域同时解决了你在编程时不一样做用域内的同名变量的问题。不要把环境/上下文做用域搞混,他们是不一样的。数组

JavaScript的做用域

JavaScript有两种类型的做用域:浏览器

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

定义在函数内部的变量在本地范围内,而定义在函数外部的变量的做用域是全局。每一个函数的触发调用都会建立一个新的做用域。安全

全局做用域

当你开始写JS的时候,你就已经处在全局范围内了,一个变量若不在函数内,即是全局变量。闭包

// the scope is by default global
var name = 'Hammad';

全局范围内的变量能够在其余范围内获取或修改。app

var name = 'Hammad';

console.log(name); // logs 'Hammad'

function logName() {
    console.log(name); // 'name' is accessible here and everywhere else
}

logName(); // logs 'Hammad'

局部做用域

定义在函数内的变量就在局部做用域。
每次调用那个函数他们都有不一样的做用域,也就是说同名变量能够在不一样的函数内使用。由于这些变量与他们各自的函数绑定,各自有不一样的做用域,没法在其余函数内获取。编程语言

// Global Scope
function someFunction() {
    // Local Scope #1
    function someOtherFunction() {
        // Local Scope #2
    }
}

// Global Scope
function anotherFunction() {
    // Local Scope #3
}
// Global Scope

块语句

ifswitch这种条件语句或forwhile这种循环语句————非函数的块语句,不会创造新的做用域。定义在块语句内的变量将保持他们当前的做用域。

if (true) {
    // this 'if' conditional block doesn't create a new scope
    var name = 'Hammad'; // name is still in the global scope
}

console.log(name); // logs 'Hammad'
ECMAScript 6引入了 letconst关键字,能够用于替换 var。相比 var,后者支持 块做用域的声明。
if (true) {
    // this 'if' conditional block doesn't create a scope

    // name is in the global scope because of the 'var' keyword
    var name = 'Hammad';
    // likes is in the local scope because of the 'let' keyword
    let likes = 'Coding';
    // skills is in the local scope because of the 'const' keyword
    const skills = 'JavaScript and PHP';
}

console.log(name); // logs 'Hammad'
console.log(likes); // Uncaught ReferenceError: likes is not defined
console.log(skills); // Uncaught ReferenceError: skills is not defined
只要你的应用激活了,全局做用域也就激活了。局部做用域则随着你的函数的调用和执行而激活。

Context————上下文/环境

许多开发者常常把做用域和上下文弄混淆,好像它们是相同的概念。非也。做用域就是咱们以上讨论的,而上下文是指你得代码特定区域内this的值。做用域指变量的可见性,上下文指同一范围下this的值。咱们能够用函数方法改变上下文,这个稍后讨论。在全局范围内,上下文老是window对象。

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

function logFunction() {
    console.log(this);
}
// logs: Window {speechSynthesis: SpeechSynthesis, caches: CacheStorage, localStorage: Storage…}
// because logFunction() is not a property of an object
logFunction();

若是做用域是一个对象的方法,上下文就是方法所属的对象。

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

(new User).logName(); // logs User {}
(new User).logName() 是一个在变量中存储对象并调用 logName的快捷的方法。这里你不须要建立一个新变量。
你可能会注意到一件事情:若是你用 new关键字调用函数,上下文的值会改变为所调用的函数的实例。例如:
function logFunction() {
    console.log(this);
}

new logFunction(); // logs logFunction {}

严格模式下上下文默认为undefined

  • 将"use strict"放在脚本文件的第一行,则整个脚本都将以"严格模式"运行。若是这行语句不在第一行,则无效,整个脚本以"正常模式"运行。
  • 若是不一样模式的代码文件合并成一个文件,这一点须要特别注意。(严格地说,只要前面不是产生实际运行结果的语句,"use strict"能够不在第一行,好比直接跟在一个空的分号后面。)将"use strict"放在函数体的第一行,则整个函数以"严格模式"运行。
  • 对于脚本,最好将整个脚本文件放在一个当即执行的匿名函数之中。

执行上下文

为了完全弄清楚以上困惑,在执行上下文中的上下文指的是做用域而不是上下文。这是个奇怪的命名惯例可是由于JavaScript已经明确了它,咱们只需记住便可。
JavaScript是一个单线程语言因此他一次只能执行一个任务。剩下的任务在执行上下文中以队列形式存在。正如我以前所说,当JavaScript编译器开始执行代码时,上下文(做用域)就被默认设置为全局的了。这个全局的上下文会添加在执行上下文中,它其实是启动执行上下文的第一个上下文。
随后,
每一个函数请求会添加它的上下文到执行上下文。当函数内的另外一个函数或其余地方的函数调用时也同样。

每一个函数都会建立本身的执行上下文。
一旦浏览器执行完上下文的代码,上下文会从执行上下文中弹出, 在执行上下文中的当前上下文的状态会被传递给父级上下文。浏览器总会执行在任务栈最顶端的执行上下文(也就是你代码中最内部的做用域)。
只能有一个全局上下文但函数上下文能够有多个。

执行上下文有两个阶段:建立 和 执行。

建立阶段

第一个阶段是建立阶段,是指函数被调用尚未被执行的时期,在建立阶段会作三件事情:

  1. 建立变量对象
  2. 建立做用域链
  3. 设置上下文的值(this

代码执行阶段

第二个阶段是代码执行阶段,这个阶段将为变量赋值,最终执行代码。

词法域

词法域是指在一组函数中,内部函数能够获取到他的父级做用域内的变量和其余资源。这意味这子函数在词法上绑定了父级的执行上下文。词法域有时也指静态域。

function grandfather() {
    var name = 'Hammad';
    // likes is not accessible here
    function parent() {
        // name is accessible here
        // likes is not accessible here
        function child() {
            // Innermost level of the scope chain
            // name is also accessible here
            var likes = 'Coding';
        }
    }
}

您将注意到词法域提早工做,意思是能够经过它的孩子的执行上下文访问name。但它在其父级无效,意味着likes不能被父级访问获取。也就是说,同名变量内部函数的优先权高于外层函数。

闭包

闭包的概念与词法域关系紧密。当一个内部函数试图访问外部函数的做用域链即其词法域外的变量值时,闭包就会被建立了。闭包包含他们本身的的做用域链,他们父级做用域链以及全局的做用域。闭包就是可以读取其余函数内部变量的函数,因为在Javascript语言中,只有函数内部的子函数才能读取局部变量,所以能够把闭包简单理解成"定义在一个函数内部的函数"。

闭包不只能够获取函数内部的变量,也能够获取其外部函数的参数资源。
 var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      return function(){
        return this.name;
      };

    }

  };

  alert(object.getNameFunc()());   // =>The Window
 var name = "The Window";

  var object = {
    name : "My Object",

    getNameFunc : function(){
      var that = this;
      return function(){
        return that.name;
      };

    }

  };

  alert(object.getNameFunc()());  // My Object

闭包甚至在函数已经返回后也能够获取其外部函数的变量。这容许返回函数一直能够获取其外部函数的全部资源。

当一个函数返回一个内部函数时,即便你调用外部函数时返回函数并不会被请求执行。你必须用一个独立的变量保存外部函数的调用请求,而后以函数形式调用该变量:

function greet() {
    name = 'Hammad';
    return function () {    //这个函数就是闭包
        console.log('Hi ' + name);
    }
}

greet(); // nothing happens, no errors

// the returned function from greet() gets saved in greetLetter
greetLetter = greet();

 // calling greetLetter calls the returned function from the greet() function
greetLetter(); // logs 'Hi Hammad'

一样能够用()()替换变量分配执行的过程。

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

greet()(); // logs 'Hi Hammad'

闭包最大用处有两个,一个是前面提到的能够读取函数内部的变量,另外一个就是让这些变量的值始终保持在内存中

公共域和私有域

在许多其余编程语言中,你能够用 public, private and protected设置属性和类的方法的可见性。JavaScript中没有相似的公共域和私有域的机制。可是咱们能够用闭包模拟这种机制,为了将全部资源与全局域独立开来,应该这样封装函数:

(function () {
  // private scope
})();

()在函数最后是告诉编译器直接在读到该函数时不用等到函数调用就执行它,咱们能够在里面添加函数和变量而不用担忧他们被外部获取到。可是若是咱们想让外部获取它们即想暴露部分变量或函数供外部修改获取怎么办?模块模式————闭包的一种,支持咱们在一个对象内利用公共域和私有域访问审视咱们的函数。

模块模式

模块模式:

var Module = (function() {
    function privateMethod() {
        // do something
    }

    return {
        publicMethod: function() {
            // can call privateMethod();
        }
    };
})();

模块的返回语句包含了咱们的公共函数。那些没有返回的即是私有函数。没有返回函数使得它们在模块命名空间外没法被存取。可是公共函数能够存取方便咱们的辅助函数,ajax请求以及其余须要的函数。

Module.publicMethod(); // works
Module.privateMethod(); // Uncaught ReferenceError: privateMethod is not defined
一个惯例是私有函数的命名通常以 __开头并返回一个包含公共函数的匿名对象。
var Module = (function () {
    function _privateMethod() {
        // do something
    }
    function publicMethod() {
        // do something
    }
    return {
        publicMethod: publicMethod,
    }
})();

Immediately-Invoked Function Expression (IIFE)当即调用函数表达式

另外一种形式的闭包叫当即调用的函数表达式,这是一个在window上下文中自我触发的匿名函数,意思就是this的值是window。它能够暴露一个可交互的全局接口。

(function(window) {
        // do anything
    })(this);

一种常见的闭包致使的bug由当即调用函数表达式解决的例子:

// This example is explained in detail below (just after this code box).​
​function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
      theCelebrities[i]["id"] = function ()  {
        return uniqueID + i;
      }
    }
    
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id()); // 103

事实上结果的全部id都是103,而不是按顺序得出的101,102,103...。
由于for循环中的匿名函数获得是外部函数变量的引用而非变量实际值,而i的值最终结果为3,故全部id103,这样修改能够获得预想效果:

function celebrityIDCreator (theCelebrities) {
    var i;
    var uniqueID = 100;
    for (i = 0; i < theCelebrities.length; i++) {
        theCelebrities[i]["id"] = function (j)  { // the j parametric variable is the i passed in on invocation of this IIFE​
            return function () {
                return uniqueID + j; // each iteration of the for loop passes the current value of i into this IIFE and it saves the correct value to the array​
            } () // BY adding () at the end of this function, we are executing it immediately and returning just the value of uniqueID + j, instead of returning a function.​
        } (i); // immediately invoke the function passing the i variable as a parameter​
    }
​
    return theCelebrities;
}
​
​var actionCelebs = [{name:"Stallone", id:0}, {name:"Cruise", id:0}, {name:"Willis", id:0}];
​
​var createIdForActionCelebs = celebrityIDCreator (actionCelebs);
​
​var stalloneID = createIdForActionCelebs [0];

console.log(stalloneID.id); // 100​
​
​var cruiseID = createIdForActionCelebs [1];
console.log(cruiseID.id); // 101

利用.call(), .apply().bind()改变上下文

CallApply 函数 在调用函数时能够用来改变上下文。这赋予了你难以置信的编程能力。为了使用两个函数,你须要在函数上调用它而非用()触发,并将上下文做为第一个参数传递。函数自己的参数可在上下文后传递。

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

hello(); // the way you usually call it
hello.call(context); // here you can pass the context(value of this) as the first argument
hello.apply(context); // here you can pass the context(value of this) as the first argument

.call().apply()的不一样之处在于,在传递剩余参数时,.call()将剩余参数以,隔开,而.appley()会将这些参数包含在一个数组里传递。

function introduce(name, interest) {
    console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
    console.log('The value of this is '+ this +'.')
}

introduce('Hammad', 'Coding'); // the way you usually call it
introduce.call(window, 'Batman', 'to save Gotham'); // pass the arguments one by one after the contextt
introduce.apply('Hi', ['Bruce Wayne', 'businesses']); // pass the arguments in an array after the context

// Output:
// Hi! I'm Hammad and I like Coding.
// The value of this is [object Window].
// Hi! I'm Batman and I like to save Gotham.
// The value of this is [object Window].
// Hi! I'm Bruce Wayne and I like businesses.
// The value of this is Hi.
在效果上, call的速度要略快于 apply

下面展现了文档内的一组列表并在命令行打印它们:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Things to learn</title>
</head>
<body>
    <h1>Things to Learn to Rule the World</h1>
    <ul>
        <li>Learn PHP</li>
        <li>Learn Laravel</li>
        <li>Learn JavaScript</li>
        <li>Learn VueJS</li>
        <li>Learn CLI</li>
        <li>Learn Git</li>
        <li>Learn Astral Projection</li>
    </ul>
    <script>
        // Saves a NodeList of all list items on the page in listItems
        var listItems = document.querySelectorAll('ul li');
        // Loops through each of the Node in the listItems NodeList and logs its content
        for (var i = 0; i < listItems.length; i++) {
          (function () {
            console.log(this.innerHTML);
          }).call(listItems[i]);
        }

        // Output logs:
        // Learn PHP
        // Learn Laravel
        // Learn JavaScript
        // Learn VueJS
        // Learn CLI
        // Learn Git
        // Learn Astral Projection
    </script>
</body>
</html>

这里我想起来之前看到过的.caller().callee():

  • .caller()是指调用函数的函数体,返回函数体,相似于toString()
  • .callee()Arguments的一个成员,表示对函数对象自己的引用,经常使用属性是lengtharguments.length是指实参长度,callee.length形参长度。

具体可参考这里

对象能够有方法,一样函数对象也能够有方法。事实上,一个JavaScript函数生来就有四种内置函数

  • Function.prototype.apply()
  • Function.prototype.bind() (Introduced in ECMAScript 5 (ES5))
  • Function.prototype.call()
  • Function.prototype.toString() 将函数字符串化
.prototype => .__proto__

不一样于CallApplyBind自己不调用函数,只用来在调用函数前绑定上下文的值和其余参数,例如:

(function introduce(name, interest) {
    console.log('Hi! I\'m '+ name +' and I like '+ interest +'.');
    console.log('The value of this is '+ this +'.')
}).bind(window, 'Hammad', 'Cosmology')();

// logs:
// Hi! I'm Hammad and I like Cosmology.
// The value of this is [object Window].

Bind就像Call函数,在传递剩余的参数时以,隔开而不像Apply传递一个数组,它返回的是一个新函数

var person1 = {firstName: 'Jon', lastName: 'Kuperman'};
var person2 = {firstName: 'Kelly', lastName: 'King'};

function say() {
    console.log('Hello ' + this.firstName + ' ' + this.lastName);
}

var sayHelloJon = say.bind(person1);
var sayHelloKelly = say.bind(person2);

sayHelloJon(); // Hello Jon Kuperman
sayHelloKelly(); // Hello Kelly King

Happy Coding!

相关文章
相关标签/搜索