精华内容
下载资源
问答
  • js执行上下文

    千次阅读 2019-05-22 10:03:18
    1、内存空间 内存空间大家都比较熟,即栈与堆。 JS的基础数据类型有Undefined、Null、Boolean、Number、String,这些都是按值访问,存放在栈内存。...2、JS代码的执行环境(执行上下文,Execution Con...

    1、内存空间

    内存空间大家都比较熟,即栈与堆。
    JS的基础数据类型有Undefined、Null、Boolean、Number、String,这些都是按值访问,存放在栈内存。
    其他的Object为引用类型,如数组Array或其他的自定义对象,这些存放在堆内存,其对应的地址引用(指针)放在栈内存。
    大家对这些应该比较熟,就不赘述了。

    2、JS代码的执行环境(执行上下文,Execution Context,下面简称EC)

    JS是单线程的,运行在全局EC,每进入一个function,就做一次入栈操作,向栈顶压入一个属于该function的新的EC。若function中又调用了另一个function,则再执行一次入栈…依次执行完再依次出栈,回到全局EC。全局EC一定是在栈底,在浏览器关闭后出栈。

    执行上下文,出入栈图解:

    EC的构成如下图: 

    JavaScript对于作用域(Scope)和上下文(Context)的实现是这门语言的一个非常独到的地方,部分归功于其独特的灵活性。 函数可以接收不同的的上下文和作用域。这些概念为JavaScript中的很多强大的设计模式提供了坚实的基础。 然而这也概念也非常容易给开发人员带来困惑。为此,本文将全面的剖析这些概念,并阐述不同的设计模式是如何利用它们的。

    作用域(Scope)和上下文(Context)

    首先需要知道的是,上下文和作用域是两个完全不同的概念。多年来,我发现很多开发者会混淆这两个概念(包括我自己), 错误的将两个概念混淆了。平心而论,这些年来很多术语都被混乱的使用了。

    函数的每次调用都有与之紧密相关的作用域和上下文。从根本上来说,作用域是基于函数的,而上下文是基于对象的。 换句话说,作用域涉及到所被调用函数中的变量访问,并且不同的调用场景是不一样的。上下文始终是this关键字的值, 它是拥有(控制)当前所执行代码的对象的引用。

    变量作用域

    一个变量可以被定义在局部或者全局作用域中,这建立了在运行时(runtime)期间变量的访问性的不同作用域范围。 任何被定义的全局变量,意味着它需要在函数体的外部被声明,并且存活于整个运行时(runtime),并且在任何作用域中都可以被访问到。 在ES6之前,局部变量只能存在于函数体中,并且函数的每次调用它们都拥有不同的作用域范围。 局部变量只能在其被调用期的作用域范围内被赋值、检索、操纵。

    需要注意,在ES6之前,JavaScript不支持块级作用域,这意味着在if语句、switch语句、for循环、while循环中无法支持块级作用域。 也就是说,ES6之前的JavaScript并不能构建类似于Java中的那样的块级作用域(变量不能在语句块外被访问到)。但是, 从ES6开始,你可以通过let关键字来定义变量,它修正了var关键字的缺点,能够让你像Java语言那样定义变量,并且支持块级作用域。看两个例子:

    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默认情况下是全局上下文,在浏览器中它指向window对象。需要注意的是,ES5引入了严格模式的概念, 如果启用了严格模式,此时上下文默认为undefined

    执行环境(execution context)

    JavaScript是一个单线程语言,意味着同一时间只能执行一个任务。当JavaScript解释器初始化执行代码时, 它首先默认进入全局执行环境(execution context),从此刻开始,函数的每次调用都会创建一个新的执行环境。

    这里会经常引起新手的困惑,这里提到了一个新的术语——执行环境(execution context),它定义了变量或函数有权访问的其他数据,决定了它们各自的行为。 它更偏向于作用域的作用,而不是我们前面讨论的上下文(Context)。请务必仔细的区分执行环境和上下文这两个概念(注:英文容易造成混淆)。 说实话,这是个非常糟糕的命名约定,但是它是ECMAScript规范制定的,你还是遵守吧。

    每个函数都有自己的执行环境。当执行流进入一个函数时,函数的环境就会被推入一个环境栈中(execution stack)。在函数执行完后,栈将其环境弹出, 把控制权返回给之前的执行环境。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('div');
        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
    });

     

    apply、call

    在 javascript 中,call 和 apply 都是为了改变某个函数运行时的上下文(context)而存在的,换句话说,就是为了改变函数体内部 this 的指向。
    JavaScript 的一大特点是,函数存在「定义时上下文」和「运行时上下文」以及「上下文是可以改变的」这样的概念。

    function fruits() {}
     
    fruits.prototype = {
        color: "red",
        say: function() {
            console.log("My color is " + this.color);
        }
    }
     
    var apple = new fruits;
    apple.say();    //My color is red

    但是如果我们有一个对象banana= {color : "yellow"} ,我们不想对它重新定义 say 方法,那么我们可以通过 call 或 apply 用 apple 的 say 方法:

    banana = {
        color: "yellow"
    }
    apple.say.call(banana);     //My color is yellow
    apple.say.apply(banana);    //My color is yellow

    所以,可以看出 call 和 apply 是为了动态改变 this 而出现的,当一个 object 没有某个方法(本栗子中banana没有say方法),但是其他的有(本栗子中apple有say方法),我们可以借助call或apply用其它对象的方法来操作。

    apply、call 区别

    对于 apply、call 二者而言,作用完全一样,只是接受参数的方式不太一样。例如,有一个函数定义如下:

    var func = function(arg1, arg2) {
         
    };

    就可以通过如下方式来调用:

    func.call(this, arg1, arg2);
    func.apply(this, [arg1, arg2])

    其中 this 是你想指定的上下文,他可以是任何一个 JavaScript 对象(JavaScript 中一切皆对象),call 需要把参数按顺序传递进去,而 apply 则是把参数放在数组里。  
    为了巩固加深记忆,下面列举一些常用用法:

    apply、call实例

    数组之间追加

    var array1 = [12 , "foo" , {name:"Joe"} , -2458]; 
    var array2 = ["Doe" , 555 , 100]; 
    Array.prototype.push.apply(array1, array2); 
    // array1 值为  [12 , "foo" , {name:"Joe"} , -2458 , "Doe" , 555 , 100] 

    获取数组中的最大值和最小值

    var  numbers = [5, 458 , 120 , -215 ]; 
    var maxInNumbers = Math.max.apply(Math, numbers),   //458
        maxInNumbers = Math.max.call(Math,5, 458 , 120 , -215); //458

    number 本身没有 max 方法,但是 Math 有,我们就可以借助 call 或者 apply 使用其方法。

    验证是否是数组(前提是toString()方法没有被重写过)

    functionisArray(obj){ 
        return Object.prototype.toString.call(obj) === '[object Array]' ;
    }

    类(伪)数组使用数组方法

    var domNodes = Array.prototype.slice.call(document.getElementsByTagName("*"));

    Javascript中存在一种名为伪数组的对象结构。比较特别的是 arguments 对象,还有像调用 getElementsByTagName , document.childNodes 之类的,它们返回NodeList对象都属于伪数组。不能应用 Array下的 push , pop 等方法。
    但是我们能通过 Array.prototype.slice.call 转换为真正的数组的带有 length 属性的对象,这样 domNodes 就可以应用 Array 下的所有方法了。

    面试题

    定义一个 log 方法,让它可以代理 console.log 方法,常见的解决方法是:

    function log(msg) {
      console.log(msg);
    }
    log(1);    //1
    log(1,2);    //1

    上面方法可以解决最基本的需求,但是当传入参数的个数是不确定的时候,上面的方法就失效了,这个时候就可以考虑使用 apply 或者 call,注意这里传入多少个参数是不确定的,所以使用apply是最好的,方法如下:

    function log(){
      console.log.apply(console, arguments);
    };
    log(1);    //1
    log(1,2);    //1 2

     接下来的要求是给每一个 log 消息添加一个"(app)"的前辍,比如:

    log("hello world"); //(app)hello world

    该怎么做比较优雅呢?这个时候需要想到arguments参数是个伪数组,通过 Array.prototype.slice.call 转化为标准数组,再使用数组方法unshift,像这样:

    function log(){
      var args = Array.prototype.slice.call(arguments);
      args.unshift('(app)');
     
      console.log.apply(console, args);
    };

    bind

    在讨论bind()方法之前我们先来看一道题目:

    var altwrite = document.write;
    altwrite("hello");

    结果:Uncaught TypeError: Illegal invocation
    altwrite()函数改变this的指向global或window对象,导致执行时提示非法调用异常,正确的方案就是使用bind()方法:

    altwrite.bind(document)("hello")

    当然也可以使用call()方法:

    altwrite.call(document, "hello")

    绑定函数

    bind()最简单的用法是创建一个函数,使这个函数不论怎么调用都有同样的this值。常见的错误就像上面的例子一样,将方法从对象中拿出来,然后调用,并且希望this指向原来的对象。如果不做特殊处理,一般会丢失原来的对象。使用bind()方法能够很漂亮的解决这个问题:

    this.num = 9; 
    var mymodule = {
      num: 81,
      getNum: function() { 
        console.log(this.num);
      }
    };
    
    mymodule.getNum(); // 81
    
    var getNum = mymodule.getNum;
    getNum(); // 9, 因为在这个例子中,"this"指向全局对象
    
    var boundGetNum = getNum.bind(mymodule);
    boundGetNum(); // 81

    bind() 方法与 apply 和 call 很相似,也是可以改变函数体内 this 的指向。

    MDN的解释是:bind()方法会创建一个新函数,称为绑定函数,当调用这个绑定函数时,绑定函数会以创建它时传入 bind()方法的第一个参数作为 this,传入 bind() 方法的第二个以及以后的参数加上绑定函数运行时本身的参数按照顺序作为原函数的参数来调用原函数。

    直接来看看具体如何使用,在常见的单体模式中,通常我们会使用 _this , that , self 等保存 this ,这样我们可以在改变了上下文之后继续引用到它。 像这样:

    var foo = {
        bar : 1,
        eventBind: function(){
            var _this = this;
            $('.someClass').on('click',function(event) {
                /* Act on the event */
                console.log(_this.bar);     //1
            });
        }
    }

    由于 Javascript 特有的机制,上下文环境在 eventBind:function(){ } 过渡到 $('.someClass').on('click',function(event) { }) 发生了改变,上述使用变量保存 this 这些方式都是有用的,也没有什么问题。当然使用 bind() 可以更加优雅的解决这个问题:

    var foo = {
        bar : 1,
        eventBind: function(){
            $('.someClass').on('click',function(event) {
                /* Act on the event */
                console.log(this.bar);      //1
            }.bind(this));
        }
    }

    在上述代码里,bind() 创建了一个函数,当这个click事件绑定在被调用的时候,它的 this 关键词会被设置成被传入的值(这里指调用bind()时传入的参数)。因此,这里我们传入想要的上下文 this(其实就是 foo ),到 bind() 函数中。然后,当回调函数被执行的时候, this 便指向 foo 对象。再来一个简单的栗子:

    var bar = function(){
    console.log(this.x);
    }
    var foo = {
    x:3
    }
    bar(); // undefined
    var func = bar.bind(foo);
    func(); // 3

    这里我们创建了一个新的函数 func,当使用 bind() 创建一个绑定函数之后,它被执行的时候,它的 this 会被设置成 foo , 而不是像我们调用 bar() 时的全局作用域。

    偏函数(Partial Functions)

    Partial Functions也叫Partial Applications,这里截取一段关于偏函数的定义:

    Partial application can be described as taking a function that accepts some number of arguments, binding values to one or more of those arguments, and returning a new function that only accepts the remaining, un-bound arguments.

    bind()的另一个最简单的用法是使一个函数拥有预设的初始参数。只要将这些参数(如果有的话)作为bind()的参数写在this后面。当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置,传递给绑定函数的参数会跟在它们后面。

    function list() {
      return Array.prototype.slice.call(arguments);
    }
    
    var list1 = list(1, 2, 3); // [1, 2, 3]
    
    // 预定义参数37
    var leadingThirtysevenList = list.bind(undefined, 37);
    
    var list2 = leadingThirtysevenList(); // [37]
    var list3 = leadingThirtysevenList(1, 2, 3); // [37, 1, 2, 3]

    和setTimeout一起使用

    function Bloomer() {
      this.petalCount = Math.ceil(Math.random() * 12) + 1;
    }
    
    // 1秒后调用declare函数
    Bloomer.prototype.bloom = function() {
      window.setTimeout(this.declare.bind(this), 100);
    };
    
    Bloomer.prototype.declare = function() {
      console.log('我有 ' + this.petalCount + ' 朵花瓣!');
    };
    
    var bloo = new Bloomer();
    bloo.bloom(); //我有 5 朵花瓣!

    注意:对于事件处理函数和setInterval方法也可以使用上面的方法

    绑定函数作为构造函数

    绑定函数也适用于使用new操作符来构造目标函数的实例。当使用绑定函数来构造实例,注意:this会被忽略,但是传入的参数仍然可用。

    function Point(x, y) {
      this.x = x;
      this.y = y;
    }
    
    Point.prototype.toString = function() { 
      console.log(this.x + ',' + this.y);
    };
    
    var p = new Point(1, 2);
    p.toString(); // '1,2'
    
    
    var emptyObj = {};
    var YAxisPoint = Point.bind(emptyObj, 0/*x*/);
    // 实现中的例子不支持,
    // 原生bind支持:
    var YAxisPoint = Point.bind(null, 0/*x*/);
    
    var axisPoint = new YAxisPoint(5);
    axisPoint.toString(); // '0,5'
    
    axisPoint instanceof Point; // true
    axisPoint instanceof YAxisPoint; // true
    new Point(17, 42) instanceof YAxisPoint; // true

    捷径

    bind()也可以为需要特定this值的函数创造捷径。

    例如要将一个类数组对象转换为真正的数组,可能的例子如下:

    var slice = Array.prototype.slice;
    
    // ...
    
    slice.call(arguments);

    如果使用bind()的话,情况变得更简单:

    var unboundSlice = Array.prototype.slice;
    var slice = Function.prototype.call.bind(unboundSlice);
    
    // ...
    
    slice(arguments);

    实现

    上面的几个小节可以看出bind()有很多的使用场景,但是bind()函数是在 ECMA-262 第五版才被加入;它可能无法在所有浏览器上运行。这就需要我们自己实现bind()函数了。

    首先我们可以通过给目标函数指定作用域来简单实现bind()方法:

    Function.prototype.bind = function(context){
      self = this;  //保存this,即调用bind方法的目标函数
      return function(){
          return self.apply(context,arguments);
      };
    };

    考虑到函数柯里化的情况,我们可以构建一个更加健壮的bind():

    Function.prototype.bind = function(context){
      var args = Array.prototype.slice.call(arguments, 1),
      self = this;
      return function(){
          var innerArgs = Array.prototype.slice.call(arguments);
          var finalArgs = args.concat(innerArgs);
          return self.apply(context,finalArgs);
      };
    };

    这次的bind()方法可以绑定对象,也支持在绑定的时候传参。

    继续,Javascript的函数还可以作为构造函数,那么绑定后的函数用这种方式调用时,情况就比较微妙了,需要涉及到原型链的传递:

    Function.prototype.bind = function(context){
      var args = Array.prototype.slice(arguments, 1),
      F = function(){},
      self = this,
      bound = function(){
          var innerArgs = Array.prototype.slice.call(arguments);
          var finalArgs = args.concat(innerArgs);
          return self.apply((this instanceof F ? this : context), finalArgs);
      };
    
      F.prototype = self.prototype;
      bound.prototype = new F();
      return bound;
    };

    这是《JavaScript Web Application》一书中对bind()的实现:通过设置一个中转构造函数F,使绑定后的函数与调用bind()的函数处于同一原型链上,用new操作符调用绑定后的函数,返回的对象也能正常使用instanceof,因此这是最严谨的bind()实现。

    对于为了在浏览器中能支持bind()函数,只需要对上述函数稍微修改即可:

    if (!Function.prototype.bind) {
      Function.prototype.bind = function(oThis) {
        if (typeof this !== 'function') {
          // closest thing possible to the ECMAScript 5
          // internal IsCallable function
          throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
        }
    
        var aArgs   = Array.prototype.slice.call(arguments, 1),
            fToBind = this,
            fNOP    = function() {},
            fBound  = function() {
              // this instanceof fBound === true时,说明返回的fBound被当做new的构造函数调用
              return fToBind.apply(this instanceof fBound
                     ? this
                     : oThis,
                     // 获取调用时(fBound)的传参.bind 返回的函数入参往往是这么传递的
                     aArgs.concat(Array.prototype.slice.call(arguments)));
            };
    
        // 维护原型关系
        if (this.prototype) {
          // Function.prototype doesn't have a prototype property
          fNOP.prototype = this.prototype; 
        }
        // 下行的代码使fBound.prototype是fNOP的实例,因此
        // 返回的fBound若作为new的构造函数,new生成的新对象作为this传入fBound,新对象的__proto__就是fNOP的实例
        fBound.prototype = new fNOP();
    
        return fBound;
      };
    }

    有个有趣的问题,如果连续 bind() 两次,亦或者是连续 bind() 三次那么输出的值是什么呢?像这样:

    var bar = function(){
        console.log(this.x);
    }
    var foo = {
        x:3
    }
    var sed = {
        x:4
    }
    var func = bar.bind(foo).bind(sed);
    func(); //?
     
    var fiv = {
        x:5
    }
    var func = bar.bind(foo).bind(sed).bind(fiv);
    func(); //?

    答案是,两次都仍将输出 3 ,而非期待中的 4 和 5 。原因是,在Javascript中,多次 bind() 是无效的。更深层次的原因, bind() 的实现,相当于使用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。

    apply、call、bind比较

    那么 apply、call、bind 三者相比较,之间又有什么异同呢?何时使用 apply、call,何时使用 bind 呢。简单的一个栗子:

    var obj = {
        x: 81,
    };
     
    var foo = {
        getX: function() {
            return this.x;
        }
    }
     
    console.log(foo.getX.bind(obj)());  //81
    console.log(foo.getX.call(obj));    //81
    console.log(foo.getX.apply(obj));   //81

    三个输出的都是81,但是注意看使用 bind() 方法的,他后面多了对括号。

    也就是说,区别是,当你希望改变上下文环境之后并非立即执行,而是回调执行的时候,使用 bind() 方法。而 apply/call 则会立即执行函数。

    再总结一下:

    • apply 、 call 、bind 三者都是用来改变函数的this对象的指向的;
    • apply 、 call 、bind 三者第一个参数都是this要指向的对象,也就是想指定的上下文;
    • apply 、 call 、bind 三者都可以利用后续参数传参;
    • bind 是返回对应函数,便于稍后调用;apply 、call 则是立即调用 。
    展开全文
  • 我们在JS学习初期或者面试的时候常常会遇到考核变量...暂时先不管这个例子,我们先引入一个JavaScript中最基础,但同时也是最重要的一个概念执行上下文(Execution Context)。 每次当控制器转到可执行代码的时候,就会进入.

    在这里插入图片描述
    我们在JS学习初期或者面试的时候常常会遇到考核变量提升的思考题。比如先来一个简单一点的

    console.log(a);   // 这里会打印出什么?
    var a = 20;
    
    1. 变量提升:即将变量声明提升到它所在作用域的最开始的部分
    2. 函数提升:js中创建函数有两种方式:函数声明式和函数字面量式。只有函数声明才存在函数提升

    暂时先不管这个例子,我们先引入一个JavaScript中最基础,但同时也是最重要的一个概念执行上下文(Execution Context)。

    每次当控制器转到可执行代码的时候,就会进入一个执行上下文。执行上下文可以理解为当前代码的执行环境,它会形成一个作用域。JavaScript中的运行环境大概包括三种情况。

    • 全局环境:JavaScript代码运行起来会首先进入该环境
    • 函数环境:当函数被调用执行时,会进入当前函数中执行代码
    • eval()

    因此在一个JavaScript程序中,必定会产生多个执行上下文,在我的上一篇文章中也有提到,JavaScript引擎会以堆栈的方式来处理它们。栈底永远都是全局上下文,而栈顶就是当前正在执行的上下文。

    当代码在执行过程中,遇到以上三种情况,都会生成一个执行上下文,放入栈中,而处于栈顶的上下文执行完毕之后,就会自动出栈。为了更加清晰的理解这个过程,根据下面的例子,结合图示给大家展示。

    var color = 'blue';
      
    function changeColor() {
        var anotherColor = 'red';
      
        function swapColors() {
            var tempColor = anotherColor;
            anotherColor = color;
            color = tempColor;
        }
      
        swapColors();
    }
      
    changeColor();
    

    我们用ECStock来表示处理执行上下文组的堆栈。

    第一步:全局上下文入栈

    全局上下文入栈之后,其中的可执行代码开始执行,

    在这里插入图片描述

    第二步:changeColor的执行上下文入栈

    直到遇到了changeColor(),这一句激活函数changeColor创建它自己的执行上下文,因此第二步就是changeColor的执行上下文入栈。
    changeColor的上下文入栈之后,控制器开始执行其中的可执行代码

    第三步:swapColors的执行上下文入栈

    遇到swapColors()之后又激活了一个执行上下文。因此第三步是swapColors的执行上下文入栈。

    在这里插入图片描述
    第四步:swapColors的执行上下文出栈

    在swapColors的可执行代码中,再没有遇到其他能生成执行上下文的情况,因此这段代码顺利执行完毕,swapColors的上下文从栈中弹出。
    在这里插入图片描述

    第五步:changeColor的执行上下文出栈

    swapColors的执行上下文弹出之后,继续执行changeColor的可执行代码,也没有再遇到其他执行上下文,顺利执行完毕之后弹出。这样,ECStack中就只身下全局上下文了。
    在这里插入图片描述

    全局上下文在浏览器窗口关闭后出栈。

    注意:函数中,遇到return能直接终止可执行代码的执行,因此会直接将当前上下文弹出栈。

    在这里插入图片描述
    整个过程

    详细了解了这个过程之后,我们就可以对执行上下文总结一些结论了。

    • 单线程

    • 同步执行,只有栈顶的上下文处于执行中,其他上下文需要等待

    • 全局上下文只有唯一的一个,它在浏览器关闭时出栈

    • 函数的执行上下文的个数没有限制

    • 每次某个函数被调用,就会有个新的执行上下文为其创建,即使是调用的自身函数,也是如此。
      为了巩固一下执行上下文的理解,我们再来绘制一个例子的演变过程,这是一个简单的闭包例子。

    function f1(){
        var n=999;
        function f2(){
            alert(n);
        }
        return f2;
    }
    var result=f1();
    result(); // 999
    

    因为f1中的函数f2在f1的可执行代码中,并没有被调用执行,因此执行f1时,f2不会创建新的上下文,而直到result执行时,才创建了一个新的。具体演变过程如下。
    在这里插入图片描述
    转载自:https://www.cnblogs.com/dreamingbaobei/p/9815970.html

    展开全文
  • 前端开发过程中经常会遇到作用域的问题,变量提升,闭包等等一些列的问题,那么这些问题的是怎么形成的,又是如何实现的,这里通过分析JavaScript中的执行上下文(EC)依次解开。 首先思考一段代码 var name = '...

    JavaScript执行上下文分析

    前端开发过程中经常会遇到作用域的问题,变量提升,闭包等等一些列的问题,那么这些问题的是怎么形成的,又是如何实现的,这里通过分析JavaScript中的执行上下文(EC)依次解开。

    代码思考

    var name = 'globalName';
    function F1() {
        console.log(name);
        console.log(F2);
        var name = 'f1ame';
        function F2(argumentName) {
        	console.log(argumentName);
    	}
    	F2(name);
        console.log(sex);
    }
    F1();
    

    乍一看可能会认为控制台会输出

     console.log(name); // globalName
     console.log(F2); // not defined
     console.log(argumentName); // f1ame
     console.log(sex); // not defined
    

    其实不然,上面代码执行下来的结果是

     console.log(name); // undefined
     console.log(F2); // F2(name) {console.log(name);}
     console.log(argumentName); // f1ame
     console.log(sex); // not defined
    

    ECS(执行环境栈-Execution Context Stack)

    那么输出这些的原因是什么?下面来分析一下,在分析之前,先来看一下JavaScript的压栈规则LIFO(last in first out)。当代码执行时,开始将全局上下文入ECS。当指定到函数调用时,便将函数上下文入ECS。函数上下文可以有多个,当调用时,就将调用的入ECS,运行完成时,再将函数上下文出ECS例如上方的代码,开始执行时,全局上下文入ECS,执行到F1调用时,将函数上下文F1ECS,F1中调用F2时,再将F2ECS,F2执行完成后出ECS等等。具体如下图
    在这里插入图片描述

    执行上下文形成

    JavaScript中代码的运行环境,也叫执行上下文,也就是传说中的作用域。在执行上下文(以下简称EC)中包含以下内容

    1. 作用域链Scope Chain
    2. 变量对象
    3. this指向

    EC有三种,这里主要讲前两种

    1. 全局上下文
      在JavaScript中只拥有一个全局上下文,代码最先进入的环境,在页面关闭时销毁
    2. 函数上下文
      在函数被调用时的执行环境
    3. eval
      此函数会对参数执行,会产生自己的上下文

    下面具体讲接下EC如何形成的,EC分为两个阶段,一个创建阶段,一个执行阶段

    • 创建阶段(该阶段主要分为三步)
      1. 创建作用域(Scope Chain),由当前EC和上层EC等一系列变量对象组成的层级链
      2. 创建(变量对象VO)包含参数、函数、变量
      3. this绑定(指向调用的对象)
    	第二部分VO(变量对象)创建详情:
     		初始化创建arguments:赋值;
     		声明function:赋值;
     		声明变量:undefined
    
    • 执行阶段
      1. 顺序执行代码
      2. 变量对象赋值(在这里才赋值到变量)

    以上便是一个EC的形成,接下来看一下F1函数上下文在创建阶段时什么样

    对比EC过程,进行代码详情解析

    F1EC: {
      VO: {
        Arguments: {
          // 这里便是初始化argument,有参数的话,便在这里创建
          length: 0
        },
        // 变量对象
        VO: {
          //声明引用 F2
          F2: <function fn2 reference>,
          //声明name
          name: undefined
        }
      },
      // 确定作用域链
      scopeChain: [F1EC.VO, global],
      //bindthis指向
      this: window
    }
    

    当创建阶段完成以后,接下来就是执行阶段,下面具体分析下这段代码

    // 创建全局上下文,入上下文执行栈
    var name = 'globalName';
    function F1() {
      // 第一阶段进行后
      // 这里this指向window
      // name被创建,但是未赋值
      //F2被创建并且引用
      console.log(name); // 那么这里应该是undefined
      console.log(F2); // 这里应该是 F2
      var name = 'f1ame'; // 再此之后,则name可以访问到
      
      function F2(argumentName) {
        console.log(argumentName);
      }
    
      // 创建F2上下文,入上下文执行栈 同理
      F2(name);
    
      //F2 出栈
      console.log(sex); //sex未创建,未赋值,且作用域链中也没有,所以报错 sex not defined
    }
    // 创建F1上下文,入上下文执行栈
    F1();
    // F1出栈
    // 全局上下文出栈
    

    通过上面的分析,这段代码运行结果就正确了,从这段代码中在衍生出几个JavaScript中创建的词汇解析

    变量提升

    在这里可以看到name与F2都在console.log之后才定义,但是,console.log却能够访问到。通过上面的分析可知,在代码执行之前,就已 经创建了VO对象并且函数已经赋值引用。这里就说明了变量提升的原理

    闭包

    实例解析

    全局执行上下文中无法访问局部上下文中的变量,但是可以通过闭包来实现。下面F2函数就是闭包,F2作用域链是F2、F1、window

    function F1() {
      var name = 'f1name';
      function F2() {
      	// 访问F2、F1、window
      	console.log(name); //f1name
      }
      return F2
    }
    F1()()
    // 访问全局作用域
    console.log(name) // not defined
    

    分析两道经典代码片段

    第一题

    var name = "The Window";
    
    var obj = {
      name: "My Object",
      F1: function () {
        console.log(this); //指向调用的obj,这里是obj
        return function () {
          return this.name;
        };
      }
    };
    
    var res = obj.F1(); //obj调用
    
    console.log(res()); //window调用,所以this.name === window.name
    

    第二题

    var name = "The Window";
    
    var obj = {
      name: "My Object",
      F1: function () {
        console.log(this); //指向调用的obj,这里是obj
        var that = this; // 指向obj,这里that指向obj
        return function () {
          // that依赖F1执行上下文,所以这里that.name = My Object
          return that.name;
        };
      }
    };
    
    var res = obj.F1();
    
    console.log(res()); //that.name === obj.name
    

    分析作用域

    通过console.dir分别将两题返回的res函数打印出来

    • 第一题
      在这里插入图片描述
      由图可见Scopes只存在Global一个作用域,因为return function () { return this.name; };并不存在依赖F1作用域的局部变量,所以这里只存在Global一个作用域。再加上调用res函数的上层对象的this指向全局作用域Global,所以第一题的this.name的值为,全局作用域Global里面的name:The Window。
    • 第二题
      在这里插入图片描述
      由图可见Scopes存在两个作用域,因为在闭包环境下,res函数中的变量依赖F1作用域的局部变量that,所以这里多出来一个Closure (F1)的作用域。当调用res变量时,打印that.name的值。that存在作用域Closure (F1)中,顺着作用域往上找,Closure (F1)作用域中that.name:My Object。
    展开全文
  • 执行上下文: 先看一段代码在浏览器中的执行结果: console.log(a); b(); console.log(this); var a = 1; function b(){ console.log("b") ...javascript在执行代码前,将window确定为全局执行上下文

    执行上下文:

    什么是执行上下文

    先看一段代码在浏览器中的执行结果:

    console.log(a);
    b();
    console.log(this);
    var a = 1;
    function b(){
        console.log("b")
    }
    

    执行结果:
    img

    我们都知道javascript是一种解释性语言 ,它会从上至下逐行执行,但是很明显以上代码就不是这回事。其实javascript在执行代码前,会做的一些准备工作,这就是创建对应的执行上下文,可以抽象的理解为执行上下文就是Javascript执行时存在的环境。

    执行上下文的类型

    全局执行上下文

    1. javascript在执行代码前,将window确定为全局执行上下文。

    2. 对全局的数据进行预处理:

      1)var 定义的全局变量,初始值为 undefined,造成声明提升 ,并添加为window的属性。

      2)function声明的全局函数添加为window的方法。

      3)将this的指针执行全局对象window。点击进入js中的多种this指向详解

    3. 开始执行全局代码。

    函数执行上下文

    1. 在调用函数,准备执行函数体前,临时创建对应的函数上下文对象。

    2. 对局部数据进行预处理:

      1)首先会初始化函数的参数arguments,添加为函数上下文对象的属性。

      2)var 定义的局部变量,初始值为 undefined,造成声明提升 ,并添加为函数上下文对象的属性。

      3)function声明的函数添加为函数上下文对象的方法。

      4)将this的指针指向调用函数的对象。点击进入js中的多种this指向详解

    3. 开始执行代码

    执行上下文栈

    在全局执行代码前,JS引擎会创建一个栈来存储管理所有的执行上下文对象

    在全局执行上下文(window)确定后,将其添加到栈中

    在函数执行上下文创建后,将其添加到栈中

    当前函数执行完后,将栈顶对象移除

    当所有代码执行完后,栈中只剩下window

    在这里插入图片描述

    eval函数执行上下文

    运行在eval函数中的代码也获得了自己的执行上下文。

    执行上下文的生命周期

    创建阶段 - 执行阶段 - 回收阶段

    1)创建阶段

    当函数被调用但是未执行内部的任何代码之前,会做以下几件事:

    1. 创建变量对象:首先会初始化函数的参数arguments,提升函数声明和变量声明
    2. 创建作用域连:在执行上下文创建阶段,作用域链是在变量对象之后创建的,
    3. 确定this指向

    2)指向阶段

    变量赋值,代码执行

    3)执行上下文出栈,等待垃圾回收。

    展开全文
  •   执行上下文就是当前 JS 代码被解析和执行时所在的环境的抽象概念,JS 中运行的代码都是在执行上下文中进行的。 执行上下文的类型 全局执行上下文   默认的执行上下文,一个程序中只有一个全局执行上下文。不在...
  • 详细讲解javascript执行上下文、函数堆栈、变量提升的原理,对代码解析运行过程有更深层的理解
  • 本文实例讲述了javascript执行上下文、变量对象。分享给大家供大家参考,具体如下: 突然看到一篇远在2010年的老文,作者以章节的形式向我们介绍了ECMA-262-3的部分内容,主要涉及到执行上下文、变量对象、作用域、...
  • 代码编译主要由编译器完成,而代码执行,由js引擎完成,而执行上下文,其实就是在执行阶段创建的。 代码每进入到一个运行环境,都会创建一个当前环境的执行上下文。而执行上下文在创建的时候,会做以下事情 首先会...
  • 执行上下文就是当前的 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行的. 2 执行上下文的类型 执行上下文分为三种类型: 全局执行上下文:只有一个,这是默认...
  • 执行上下文不是一个具体的东西,而是一个抽象的概念,它对于描述JavaScript编译和执行代码的过程的某些部分非常有用。虽然我们都知道JavaScript是一门解释性语言,但是浏览器执行JavaScript的过程也是需要一个编译...
  • 执行上下文和执行栈是 JavaScript 中关键概念之一,是 JavaScript 难点之一。 理解执行上下文和执行栈同样有助于理解其他的 JavaScript 概念如提升机制、作用域和闭包等。本文尽可能用通俗易懂的方式来介绍这些概念...
  • 理解执行上下文的类型与创建过程,对于理解js运行机制有极大的帮助。这篇文章将介绍执行上下文。 什么是执行上下文执行上下文是当前JavaScript代码被解析和执行时所在环境的抽象概念。 执行上下文的类型 执行上...
  • 下面小编就为大家带来一篇对于Javascript 执行上下文的全面了解。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • title: js执行上下文 前言:JavaScript没有块级作用域 开始之前,必须先声明,JavaScript 没有块级作用域,只有执行上下文。这也是这篇文章的输出原因之一,讲清楚作用域和执行上下文的关系。 请不要简单的把一个 {}...
  • 作用域 作用域是指程序源代码中定义变量的区域。 作用域规定了如何查找变量,也就是...JavaScript 采用就是词法作用域(lexical scoping),也就是静态作用域。使用词法环境来管理。 一段代码执行前,先初始化词...
  • 主要介绍了通过实例了解JS执行上下文运行原理,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
  • this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际this的最终指向的是那个调用它的对象(这句话有些问题,后面会解释为什么会有问题,虽然网上大部分的文章都是这样说的,...
  • js 执行上下文

    2019-12-01 21:20:11
    文章目录第13章 执行上下文13-1 执行上下文介绍执行上下文概念执行栈(函数调用栈)13-2 变量对象变量对象里面所拥有的东西 13-1 执行上下文介绍 执行上下文概念 什么是执行上下文 执行上下文(Execution Context):...
  • var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f();... 函数作用域是执行前就确定了,所以会从F1的作用域里找到变量a的值,为100
  • 下面的这些概念,无论是执行上下文、 还是执行栈,它在规范中的概念都很抽象,很多内容的理解实际靠的都是想象力,若有错误之处,还请指正。 执行上下文 简而言之,执行上下文(Execution Context)是正在运行的可...
  • JS执行上下文全过程

    2020-04-01 11:23:26
    var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f();...执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文...
  • Js 执行上下文

    2017-08-08 14:38:08
    主要任务是执行可执行代码,执行上下文在这个阶段创建。执行上下的生命周期1.创建阶段: 在这个阶段,执行上下文会分别创建变量对象,作用域链,确定this的指向。 2.代码执行阶段:完成变量赋值,函数引用,执行...
  • JS执行上下文

    2019-12-02 00:12:52
    JS执行上下文执行上下文概念执行上下文特点执行上下文执行上下文生命周期 先看个小例子 function fn(){ console.log(a);//undefined; var a = 1; } fn(); 为什么打印出来的是 undefined 呢? 执行上下文概念 当...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 119,158
精华内容 47,663
关键字:

js执行上下文