
建议78:正确理解执行上下文和作用域链执行上下文(execution context)是ECMAScript规范中用来描述 JavaScript 代码执行的抽象概念。所有的 JavaScript 代码都是在某个执行上下文中运行的。在当前执行上下文中调用 function会进入一个新的执行上下文。该function调用结束后会返回到原来的执行上下文中。如果function在调用过程中抛出异常,并且没有将其捕获,有可能从多个执行上下文中退出。在function调用过程中,也可能调用其他的function,从而进入新的执行上下文,由此形成一个执行上下文栈。每个执行上下文都与一个作用域链(scope chain)关联起来。该作用域链用来在function执行时求出标识符(identifier)的值。该链中包含多个对象,在对标识符进行求值的过程中,会从链首的对象开始,然后依次查找后面的对象,直到在某个对象中找到与标识符名称相同的属性。在每个对象中进行属性查找时,会使用该对象的prototype链。在一个执行上下文中,与其关联的作用域链只会被with语句和catch 子句影响。在进入一个新的执行上下文时,会按顺序执行下面的操作:(1)创建激活(activation)对象激活对象是在进入新的执行上下文时创建出来的,并且与新的执行上下文关联起来。在初始化构造函数时,该对象包含一个名为arguments的属性。激活对象在变量初始化时也会被用到。JavaScript代码不能直接访问该对象,但可以访问该对象的成员(如 arguments)。(2)创建作用域链接下来的操作是创建作用域链。每个 function 都有一个内部属性[[scope]],它的值是一个包含多个对象的链。该属性的具体值与 function 的创建方式和在代码中的位置有很大关系(见本建议后面介绍的“function 对象的创建方式”内容)。此时的主要操作是将上一步创建的激活对象添加到 function 的[[scope]]属性对应的链的前面。(3)变量初始化这一步对function中需要使用的变量进行初始化。初始化时使用的对象是创建激活对象过程中所创建的激活对象,不过此时称做变量对象。会被初始化的变量包括 function 调用时的实际参数、内部function和局部变量。在这一步中,对于局部变量,只是在变量对象中创建了同名的属性,其属性值为undefined,只有在 function 执行过程中才会被真正赋值。全局JavaScript代码是在全局执行上下文中运行的,该上下文的作用域链只包含一个全局对象。函数总是在自己的上下文环境中运行,如读/写局部变量、函数参数,以及运行内部逻辑结构等。在创建上下文环境的过程中,JavaScript会遵循一定的运行规则,并按照代码顺序完成一系列操作。这个操作过程如下:第1步,根据调用时传递的参数创建调用对象。第2步,创建参数对象,存储参数变量。第3步,创建对象属性,存储函数定义的局部变量。第4步,把调用对象放在作用域链的头部,以便检索。第5步,执行函数结构体内语句。第6步,返回函数返回值。针对上面的操作过程,下面进行详细描述。首先,在函数上下文环境中创建一个调用对象。调用对象与上下文环境是两个不同的概念,也是另一种运行机制。对象可以定义和访问自己的属性或方法,不过这里的对象不是完整意义上的对象,它没有原型,并且不能够被引用,这与Arguments对象的arguments[]数组不是真正意义上的数组一样。调用对象会根据传递的参数创建自己的Arguments对象,这是一个结构类似数组的对象,该对象内部存储着调用函数时所传递的参数。接着,创建名为arguments的属性,该属性引用刚创建的Arguments对象。然后,为上下文环境分配作用域。作用域由对象列表或对象链组成。每个函数对象都有一个内部属性(scope),这个属性值也是由对象列表或对象链组成的。 scope属性值构成了函数调用上下文环境的作用域,同时,调用对象被添加到作用域链的头部,即该对象列表的顶部(作用域链的前端)。实际上,这个头部是针对该函数的作用域链而言的,把调用对象添加到作用域的头部就是把调用对象排在函数作用域链的最上面。例如,在下面这个示例中,当调用函数e()时,将创建函数e()的调用对象和函数e()的作用域,但在调用函数e()之前,会先调用函数g(),并且生成调用函数g()的对象。而调用函数e()的对象会在函数e()的作用域范围内处于头部位置,即排在最前面。代码如下:function f(){ return e(); function e(){ return g(); function g(){ return 1; } } }alert(f()); // 1接着,正式执行函数体内代码,此时JavaScript会对函数体内创建的变量执行变量实例化操作(即转换为调用对象的属性)。下面进行具体说明。将函数的形参也创建为调用对象的命名属性,如果调用函数时传递的参数与形参一致,则将相应参数的值赋给这些命名属性,否则会将命名属性赋值为undefined。对于内部定义函数(注意其与嵌套函数的区分,两者语义不完全重合),会以其声明时所用名称为调用对象创建同名属性,对应的函数则被创建为函数对象,并将其赋值给该属性。将在函数内部声明的所有局部变量创建为调用对象的命名属性。注意,在执行函数体内的代码并计算相应的赋值表达式之前不会对局部变量进行真正的实例化。由于arguments属性与函数局部变量对应的命名属性都属于同一个调用对象,因此可以将arguments 作为函数的局部变量来看待。最后,创建this对象并对其进行赋值。如果赋值为一个对象,则this将指向该对象引用。如果赋值为null,则this就指向全局对象。创建全局上下文环境的过程与上面的描述稍微不同,因为全局上下文环境没有参数,所以不需要通过定义调用对象来引用这些参数。全局上下文环境会有一个作用域,即全局作用域,它的作用域链实际上只由一个对象组成,即全局对象(window)。全局上下文环境也会有变量实例化的过程,它的内部函数就是涉及大部分 JavaScript 代码的、常规的顶级函数声明。全局上下文环境也会使用this对象来引用全局对象。JavaScript作用域可以细分为词法作用域和动态作用域。词法作用域又称为定义作用域,这是从静态角度来说的。在函数没有被调用之前,根据函数结构的嵌套关系来确定函数的作用域。因此词法作用域取决于源代码,通常编译器可以进行静态分析来确定每个标识符实际的引用。动态作用域也称为执行作用域,这是从动态角度来说的。当函数被调用之后,其作用域会因为调用而发生变化,此时作用域链也会随之调整。定义作用域就是用来说明函数在定义时存在的嵌套关系。当函数被执行时,作用域可能会发生变化。JavaScript函数运行在它们被定义的作用域中,而不是它们被执行的作用域中。在 JavaScript 中,function 对象的创建方式有3种:function 声明、function 表达式和使用 Function 构造器。function a() {}var a = function() {}var a = new Function()通过这3种方法创建出来的 function 对象的scope属性的值有所不同,从而影响 function执行过程中的作用域链,具体说明如下:使用function语句声明的function对象是在进入执行上下文时的变量初始化过程中创建的。该对象的scope属性的值是它被创建时的执行上下文对应的作用域链。使用function表达式的function对象是在该表达式被执行的时候创建的。该对象的scope属性的值与使用function声明创建的对象一样。使用Function构造器声明一个function通常有两种方式,常用格式是var funcName = new Function(p1, p2,..., pn, body),其中 p1,p2,…,pn 表示的是该function的形式参数,body是function的内容,使用该方式的function对象是在构造器被调用的时候创建的。该对象的scope属性的值总是一个只包含全局对象的作用域链。function对象的length属性可以用来获取声明function时指定的形式参数的个数,而function对象被调用时的实际参数是通过arguments来获取的。
建议77:推荐作用域安全的构造函数构造函数其实是一个使用new运算符的函数。当使用new调用时,构造函数的内部用到的this对象会指向新创建的实例。function Person(name, age, job) { this.name = name; this.age = age; this.job = job; }var person = new Person("Nicholas", 34, 'software Engineer');在没有使用new运算符来调用构造函数的情况下,由于该this对象是在运行时绑定的,因此直接调用Person()会将该对象绑定到全局对象window上,这将导致错误属性意外增加到全局作用域上。这是由于this的晚绑定造成的,在这里this被解析成了window对象。解决这个问题的方案是创建一个作用域安全的构造函数。首先确认this对象是否为正确的类型实例,如果不是,则创建新的实例并返回。function Person(name, age, job) { //检测this对象是否是Person的实例 if(this instanceof Person) { this.name = name; this.age = age; this.job = job; } else { return new Person(name, age, job); } }如果使用的构造函数获取继承且不使用原型链,那么这个继承可能就被破坏。function Polygon(sides) { if(this instanceof Polygon) { this.sides = sides; this.getArea = function() { return 0; } } else { return new Polygon(sides); } }function Rectangle(width, height) { Polygon.call(this, 2); this.width = width; this.height = height; this.getArea = function() { return this.width * this.height; }; }var rect = new Rectangle(5, 10);alert(rect.sides); //undefinedRectangle构造函数的作用域是不安全的。在新创建一个Rectangle实例后,这个实例通过Polygon.call继承了sides属性,但由于Polygon构造函数的作用域是安全的,this对象并非是Polygon的实例,因此会创建并返回一个新的Polygon对象。由于Rectangle构造函数中的this对象并没有得到增长,同时Polygon.call返回的值没有被用到,所以Rectangle实例中不会有sides属性。构造函数配合使用原型链可以解决这个问题。function Polygon(sides) { if(this instanceof Polygon) { this.sides = sides; this.getArea = function() { return 0; } } else { return new Polygon(sides); } }function Rectangle(width, height) { Polygon.call(this, 2); this.width = width; this.height = height; this.getArea = function() { return this.width * this.height; }; }//使用原型链Rectangle.prototype = new Polygon();//还是不懂???????????var rect = new Rectangle(5, 10);alert(rect.sides); //2这时构造函数的作用域就很有用了。
建议75:函数柯里化柯里化是把接受多个参数的函数变换成接受一个单一参数的函数,并且返回一个新函数,这个新函数能够接受原函数的参数。下面可以通过例子来帮助理解。function adder(num) { return function(x) { return num + x; } }var add5 = adder(5);var add6 = adder(6);print(add5(1)); // 6print(add6(1)); //7函数adder接受一个参数,并返回一个函数,这个返回的函数可以像预期那样被调用。变量add5保存着adder(5)返回的函数,这个函数可以接受一个参数,并返回参数与5的和。柯里化在 DOM 的回调中非常有用。函数柯里化的主要功能是提供了强大的动态函数创建方法,通过调用另一个函数并为它传入要柯里化(currying)的函数和必要的参数而得到。通俗点说就是利用已有的函数,再创建一个动态的函数,该动态函数内部还是通过已有的函数来发生作用,只是传入更多的参数来简化函数的参数方面的调用。function curry(fn) { var args = [].slice.call(arguments, 1); return function() { return fn.apply(null, args.concat([].slice.call(arguments, 0))); } }function add(num1, num2) { return num1 + num2; }var newAdd = curry(add, 5);alert(newAdd(6)); //11在curry函数的内部,私有变量args相当于一个存储器,用来暂时存储在调用curry函数时所传递的参数值,这样再跟后面动态创建函数调用时的参数合并并执行,就会得到一样的效果。函数柯里化的基本方法和函数绑定是一样的:使用一个闭包返回一个函数。两者的区别在于,当函数被调用时,返回函数还需要设置一些传入的参数。function bind(fn, context) { var args = Array.prototype.slice.call(arguments, 2); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); return fn.apply(context, finalArgs); }; }创建柯里化函数的通用方式是:function curry(fn) { var args = Array.prototype.slice.call(arguments, 1); return function() { var innerArgs = Array.prototype.slice.call(arguments); var finalArgs = args.concat(innerArgs); retrun fn.apply(null, finalArgs); }; }curry函数的主要功能就是将被返回的函数的参数进行排序。为了获取第一个参数后的所有参数,在arguments对象上调用slice()方法,并传入参数1,表示被返回的数组的第一个元素应该是第二个参数。
建议74:使用高阶函数高阶函数作为函数式编程众多风格中的一项显著特征,经常被使用。实际上,高阶函数即对函数的进一步抽象。高阶函数至少满足下列条件之一:接受函数作为输入。输出一个函数。 在函数式语言中,函数不但是一种特殊的对象,还是一种类型,因此函数本身是一个可以传来传去的值。也就是说,某个函数在刚开始执行的时候,总可以送入一个函数的参数。传入的参数本身就是一个函数。当然,这个输入的函数相当于某个函数的另外一个函数。当函数执行完毕之后,又可以返回另外一个新的函数,这个返回函数取决于return fn(){...}。上述过程出现3个不同的函数,分别有不同的角色。要达到这样的应用目的,需要把函数作为一个值来看待。JavaScript不但是一门灵活的语言,而且是一门精巧的函数式语言。下面看一个函数作为参数的示例。document.write([2,3,1,4].sort()); //"1,2,3,4" 这是最简单的数组排序语句。实际上Array.prototype.sort()还能够支持一个可选的参数“比较函数”,其形式如sort(fn)。fn是一个函数类型的值,说明这里应用到高阶函数。再如,下面这个对日期类型排序的sort()。// 声明3个对象,每个对象都有属性id和date var a = new Object(); var b = new Object(); var c = new Object(); a.id = 1; b.id = 2; c.id = 3; a.date = new Date(2012,3,12); b.date = new Date(2012,1,15); c.date = new Date(2012,2,10); // 存放在arr数组中 var arr = [a, b, c]; //开始调试,留意id的排列是按1、2、3这样的顺序的 arr.sort( function (x,y) { return x.date-y.date; } ); //已经对arr排序了,发现元素顺序发生变化,id也发生变化。排序是按照日期进行的在数组排序的时候就会执行“function (x,y) {return x.date-y.date; }”这个传入的函数。当没有传入任何排序参数时,默认当x大于y时返回1,当x等于y时返回0,当x小于y时返回–1。除了了解函数作为参数使用外,下面再看看函数返回值作为函数的情况。定义一个wrap函数,该函数的主要用途是产生一个包裹函数。function wrap(tag) { var stag = '<' + tag + '>'; var etag = '</' + tag.replace(/s.*/, '') + '>'; return function(x) { return stag + x + etag; } }var B = wrap('B');document.write(B('粗体字'));document.write('');document.write(wrap('B')('粗体字'));“var B = wrap('B');”这一语句已经决定了这是一个“加粗体”的特别函数,执行该B()函数就会产生 …内容…的效果。若是wrap('div'),就会产生 …内容…的效果,若是wrap('li'),就会产生…内容……的效果,依此类推。wrap('B')返回到变量B的是一个函数。若不使用变量,wrap('B')也是合法的JavaScript语句,只要最后一个括号()前面的是函数类型的值即可。为什么stag + x + etag中的stag/etag没有输入也会在wrap()内部定义?因为warp作用域中就有stag、etag两个变量。如果从理论上描述这一特性,应该属于闭包方面的内容。实际上,map()函数即为一种高阶函数,在很多的函数式编程语言中均有此函数。map(array, func)的表达式已经表明,将func函数作用于array中的每一个元素,最终返回一个新的array。应该注意的是,map对array和func的实现是没有任何预先的假设的,因此称为“高阶”函数。function map(array, func) {var res = []; for(var i = 0, len = array.length; i < len; i++) { res.push(func(array[i])); } return res; }var mapped = map([1, 3, 5, 7, 8], function(n) { return n = n + 1; });print(mapped); //2,4,6,8,9var mapped2 = map(["one", "two", "three", "four"], function(item) { return "(" + item + ")"; });print(mapped2);(one), //(two),(three),(four),为数组中的每个字符串加上括号mapped和mapped2均调用了map,但得到了截然不同的结果。因为map的参数本身已经进行了一次抽象,map函数做的是第二次抽象,所以高阶的“阶”可以理解为抽象的层次。
建议73:函数绑定有价值函数绑定就是为了纠正函数的执行上下文,特别是当函数中带有this关键字的时候,这一点尤其重要,稍微不小心,就会使函数的执行上下文发生跟预期不同的改变,导致代码执行上的错误。函数绑定具有3个特征:函数绑定要创建一个函数,可以在特定环境中以指定参数调用另一个函数。一个简单的bind()函数接收一个函数和一个环境,返回一个在给定环境中调用给定函数的函数,并且将所有参数原封不动地传递过去。被绑定函数与普通函数相比有更多的开销,它们需要更多内存,同时也因为多重函数调用而稍微慢一点,最好只在必要时使用。第一个特征常常和回调函数及事件处理函数一起使用。var handler = { message : 'Event handled', handleClick : function(event) { alert(this.message); } };var btn = document.getElementById('my-btn');EventUtil.addHandler(btn, 'click', handler.handleClick); //undefined出现上述结果的原因在于没有保存handler.handleClick()环境(上下文环境),所以this对象最后指向了DOM按钮而非handler。可以使用闭包修正此问题:var handler = { message : 'Event handled', handleClick : function(event) { alert(this.message); } };var btn = document.getElementById('my-btn');EventUtil.addHandler(btn, "click", function(event) { handler.handleClick(event); });这是特定于这段代码的解决方案。创建多个闭包可能会令代码变得难于理解和调试,因此,很多JavaScript库实现了一个可以将函数绑定到指定环境的函数bind()。bind()函数的功能是提供一个可选的执行上下文传递给函数,并且在bind()函数内部返回一个函数,以纠正在函数调用上出现的执行上下文发生的变化。最容易出现的错误就是回调函数和事件处理程序一起使用。function bind(fn, context) { return function() { return fn.apply(context, arguments); }; }在bind()中创建一个闭包,该闭包使用apply调用传入的参数,并为apply传递context对象和参数。注意:这里使用的arguments对象是内部函数的,而非bind()的。在调用返回的函数时,会在给定的环境中执行被传入的函数并给出所有参数。var handler = { message : 'Event handled', handleClick : function(event) { alert(this.message); } };var btn = document.getElementById('my-btn');EventUtil.addHandler(btn, "click", bind(handler.handlerClick, handler));
干货,收藏了,谢谢分享
干货,收藏了,谢谢分享
干货,收藏了,谢谢分享
干货,收藏了,谢谢分享
干货,收藏了,谢谢分享
干货,收藏了,谢谢分享
收藏了,干货
收藏了,干货
收藏了,干货
收藏了,干货
收藏了,干货
收藏了,干货
收藏了,干货
干货,收藏了
干货,收藏了