JavaScript权威指南(第6版)

xiaoxiao2021-02-28  123

第1章   JavaScript概述

1、存在"<="(小于等于)和">=(大于等于)"两种运算符

第3章   类型、值和变量

1.我们通常将对象称为引用类型。对象值都是引用,对象的比较均是引用的比较:当且仅当它们引用同一个基对象时,它们才相等。

var a = [];//定义一个引用空数组的变量a

var b = [];//变量b引用同一个数组

b[0] = 1;//通过变量b来修改引用的数组

a[0]; // =>1 ,变量a也会修改

a === b ;// => true,a和b引用同一个数组,因此它们相等。

就像刚看到的如上代码,将对象(或数组)赋值给一个变量,仅仅是赋值的引用值:对象本身并没有复制一次。如果你想得到一个对象或数组的副本,则必须显式复制对象的每个属性或数组的每个元素。下面这个例子则是通过循环来完成数组复制:

var a = ['a','b','c']; //待复制的数组

var b = []; //复制到的目标空数组

for(var i = 0; i<a.length; i++){ //循环遍历所以元素

    b[i] = a[i]; //将元素值复制到b中

}

2.(3.10.1)函数作用域和声明提前

在一些类似C语言的编程语言中,花括号内的每一段代码都有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们成为块级作用域,而JavaScript中没有块级作用域。JavaScript取而代之的使用了函数作用域:变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。

在如下所示代码中,在不同的位置定义了变量i,j和k,它们都在同一个作用域内——这三个变量在函数体内均是有定义的。

function test(o) {

   var i = 0; // i在整个函数体内均是由定义的

   if(typeof o == 'object' ) {

        var j = 0; //j在函数体内是有定义的,不仅仅是在这个代码段内

        for(var k = 0; k < 10; k++){  //k在函数体内是有定义的,不仅是在循环内

                 console.log(k); //输出数字0-9

        }

        console.log(k); // k已经定义了,输出10

   }

   console.log(j); // j已经定义了,但可能没有初始化

}

JavaScript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript的这个特性被非正式地称为声明提前,即JavaScript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部,看一下如下代码:

var scope = 'global';

function f(){

    console.log(scope); //输出'undefined',而不是'global'

    var scope = 'local'; //变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的

   console.log(scope); //输出'local'

}

你可能会误以为函数中的第一行会输出‘global’,因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。尽管如此,只有在程序执行到var语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的声明“提前”至函数体顶部,同时变量初始化留在原来的位置:

function f(){

   var scope;   //在函数顶部声明了局部变量

   console.log(scope);  //变量存在,但其值是"undefined"

   scope = 'local';  //这里将其初始化并赋值

   console.log(scope);  //这里它具有了我们所期望的值

}

在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于JavaScript没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。

3.(3.10.2)作为属性的变量

当声明一个JavaScript全局变量时,实际上时定义了全局对象的一个属性,当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符删除。可能你已经注意到了,如果你没有使用严格模式并给一个未声明的变量赋值的话,JavaScript会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常的可配置属性,并可以删除它们:

var truevar = 1; //声明一个不可删除的全局变量

fakevar = 2; //创建全局对象的一个可删除的属性

this.fakevar2 = 3; //同上

delete truevar  // =>false:变量并没有被删除

delete fakevar // => true: 变量被删除

delete this.fakevar2 // => true: 变量被删除

第4章 表达式和运算符

4.4属性访问表达式

属性访问表达式运算得到一个对象属性或一个数组元素的值。JavaScript为属性访问定义了两种语法:

expression.identifier

expression[expression]

第一种写法是一个表达式后跟随一个句点和标识符。表达式指定对象,标识符则指定需要访问的属性的名称。第二种写法是使用方括号,方括号内是另外一个表达式(这种方法适用于对象和数组)。第二个表达式指定要访问的属性的名称或者代表要访问数组元素的索引。

4.9相等和不相等运算符

NaN和其他任何值都是不相等的,包括它本身。通过x !== x来判断x是否为NaN,只有在x为NaN的时候,这个表达式的值才为true

4.13.3 delete运算符

var o = [1,2,3]; //定义一个数组

delete a[2]; //删除最后一个数组元素

2 in a; // => false,元素2在数组中已经不存在了

a.length // => 3,注意,数组长度并没有改变,经上一行代码删除了这个元素,但删除操作留下了一个“洞”,实际上并没有修改数组的长度,因此a数组的长度仍然是3

注意:通过var声明的变量是无法通过delete删除的

第5章 语句

5.5.4 for/in

for/in语句也使用for关键字,但它是和常规的for循环完全不同的一类循环。for/in循环语句的语法

如下:

for(variable in object)

    statement

在执行for/in语句的过程中,JavaScript解释器首先计算object表达式。如果表达式为null或者undefined,JavaScript解释器将会跳过循环并执行后续的代码。如果表达式等于一个原始值,这个原始值将会转换为与之对应的包装对象。否则,expression本身已经是对象了。JavaScript会依次枚举对象的属性来执行循环。然而在每次循环之前,JavaScript都会先计算variable表达式的值,并将属性名(一个字符串)赋值给它。

需要注意的是,只要for/in循环中variable的值可以当做赋值表达式的坐值,它可以是任意表达式。每次循环都会计算这个表达式,也就是说每次循环它计算的值有可能不同。例如,可以使用下面这段代码将所有对象属性复制至一个数组中:

var o = {x:1,y:2,z:3};

var a = [],i=0;

for(a[i++] in o) /*empty*/;

console.log(a); // Array ["x","y","z"]

JavaScript数组不过是一种特殊的对象,因此,for/in循环可以像枚举对象属性一样枚举数组索引。例如,在上面的代码之后加上这段代码就可以枚举数组的索引0,1,2:

for(i in a) console.log(i);

第6章 对象

6.1.4 Object.create()

ECMAScript 5 定义了一个名为Object.create()的方法,它创建一个新对象,其中第一个参数是这个对象的原型。

例:var o1 = Object.create({x:1,y:2}); //o1继承了属性x和y

可以通过传入参数null来创建一个没有原型的新对象,但通过这种方式创建的对象不会继承任何东西,甚至不包括基础方法,比如toString(),也就是说,它将不能和“+”运算符一起正常工作:

var o2 = Object.create(null); //o2不继承任何属性和方法

如果想创建一个普通的空对象(比如通过{}或new Object()创建的对象),需要传入Object.prototype:

var o3 = Object.create(Object.prototype); //o3和{}和new Object()一样

6.2 属性的查询和设置

可以通过点(.)或方括号([ ])运算符来获取属性的值。运算符左侧应当是一个表达式,它返回一个对象。对于点(.)来说,右侧必须是一个以属性名称命名的简单标识符。对于方括号([ ])来说,方括号内必须是一个计算结果为字符串的表达式,这个字符串就是属性的名字:

var author = book.author; //得到book的"author"属性

var title = book["main title"]; //得到book的"main title"属性

和查询属性值的写法一样,通过点和方括号也可以创建属性或给属性赋值,但需要将它们放在赋值表达式的左侧:

book.edition = 6; //给book创建一个名为"edition"的属性

book["main title"] = "ECMAScript"; //给"main title"属性赋值

当使用方括号时,我们说方括号内的表达式必须返回字符串。其实更严格地讲,表达式必须返回字符串或返回一个可以转换为字符串的值。

现在假设给对象o的属性x赋值,如果o中已经有属性x(这个属性不是继承来的),那么这个赋值操作只改变这个已有属性的值。如果o中不存在属性x,那么赋值操作给o添加一个新属性x。如果之前o继承自属性x,那么这个继承的属性就被创建的同名属性覆盖了。

属性赋值操作首先检查原型链,以此判定是否允许赋值操作。例如,如果o继承自一个只读属性x,那么赋值操作是不允许的。如果允许属性赋值操作,它也总是在原始对象上创建属性或对已有的属性赋值,而不会去修改原型链。在JavaScript中,只有在查询属性时才会体会到继承的存在,而设置属性则和继承无关,这是JavaScript的一个重要特性。

var mm = {x:1,y:2};

var nn = Object.create(mm);

var nn.x = 4;

console.log(mm.x); //=>1

console.log(nn.x); //=>4

6.3删除属性

delete运算符可以删除对象的属性。它的操作数应当是一个属性访问表达式。让人感到意外的是,delete只是断开属性和宿主对象的联系,而不会去操作属性中的属性。

a = {p:{x:1}};

b = a.p;

delete a.p;

console.log(b.x); //=>1

delete运算符只能删除自有属性,不能删除继承属性(要删除继承属性必须从定义这个属性的原型对象上删除它,而且这会影响到所有继承自这个原型的对象)。

6.4属性检测

JavaScript对象可以看做属性的集合,我们经常会检测集合中成员的所属关系——判断某个属性是否存在于某个对象中。可以通过in运算符、hasOwnPreperty()和propertyIsEnumerable()方法来完成这个工作,甚至仅通过属性查询也可以做到这一点.

in运算符的左侧是属性名(字符串),右侧是对象。如果对象的自有属性或继承属性中包含这个属性则返回true:

var o = {x:1};

"x" in o; //true

"y" in o; //false

"toString" in o; //true ,o继承toString属性

对象的hasOwnProperty()方法用来检测给定的名字是否是对象的自有属性。对于继承属性它将返回false:

var o = {x:1};

o.hasOwnProperty("x"); //true

o.hasOwnProperty("y"); //false

o.hasOwnProperty("toString"); //false,toString是继承属性

propertyIsEnumerable()是hasOwnProperty()的增强版,只有检测到是自有属性且这个属性的可枚举性(enumerable attribute)为true时它才返回true。某些内置属性时不可枚举的。通常有JavaScript代码创建的属性都是可枚举的。

var o = inherit({y:2});

o.x = 1;

o.propertyIsEnumerable("x"); //true

o.propertyIsEnumerable("y"); //false

Object.propertyIsEnumerable("toString"); //false , 不可枚举

除了使用in运算符之外,另一种更简便的方法是使用“!==”判断一个属性是否是undefined:

var o = {x:1};

o.x !== undefined; //true

o.y !== undefined; //false

o.toString !== undefined; //true

6.5枚举属性

for/in循环可以在循环体中遍历对象中所有可枚举的属性(包括自有属性和继承的属性),把属性名称赋值给循环变量。对象继承的内置方法不可枚举的,但在代码中给对象添加的属性都是可枚举的(除非用下文中提到的一个方法将它们转换为不可枚举的)。

var o = {x:1,y:2,z:3};

o.propertyIsEnumerable("toString"); // =>false

for(p in o){

    console.log(p); // 输出x,y和z,不会输出toString

}

有许多实用工具库给Object.prototype添加了新的方法或属性,这些方法和属性可以被所以对象继承并使用。然而在ECMAScript 5标准之前,这些新添加的方法是不能定义为不可枚举的,因此它们都可以在for/in循环中枚举出来。为了避免这种情况,需要过滤for/in循环返回的属性,下面两种方式是最常见的:

for(p in o) {

      if(!o.hasOwnProperty(p)) continue;  //跳过继承的属性

}

for(p in o){

      if(typeof o[p] === "function") continue; //跳过方法

}

6.7属性的特性

除了包含名字和值之外,属性还包含一些标识它们可写、可枚举和可配置的特性。

在本节里,我们将存取器属性的getter和setter方法看成是属性的特性。按照这个逻辑,我们也可以把数据属性的值同样看做属性的特性。因此,可以认为一个属性包含一个名字和4个特性。数据属性的4个特性分别是它的值(value)、可写性(writable)、可枚举性(enumerable)和可配置型(configurable)。存取器属性不具有值(value)特性和可写性,它们的可写性是由setter方法存在与否决定的。因此存取器属性的4个特性是读取(get)、写入(set)、可枚举性和可配置性。

为了实现属性特性的查询和设置操作,ECMAScript 5 中定义了一个名为“属性描述符”(property descriptor)的对象,这个对象代表那4个特性。描述符对象的属性和它们所描述的属性特性是同名的。因此,数据属性的描述符对象的属性有value、writable、enumerable和configurable。存取器属性的描述符对象则用get属性和set属性代替value和writable。其中writable、enumerable和configurable都是布尔值,当然,get属性和set属性时函数值。

通过调用Object.getOwnPropertyDescriptor()可以获得某个对象特定属性的属性描述符:

Object.getOwnPropertyDescriptor({x:1},"x"); //返回{value:1,writable:true,enumerable:true,configurable:true}

var aa = {

    name:'aa', 

    age:18,

    get dd(){

       return this.name;

    },

    set dd(s){

         this.age= s;

    },

    set ee(s){

        

    }

}

console.log(Object.getOwnPropertyDescriptor(aa,"dd"));  //{get:get dd(), set:set dd(),enumerable:true,configurable:true}

console.log(Object.getOwnPropertyDescriptor(aa,"ee")); //{get:undefined,set:set ee(),enumerable:true,configurable:true}

//对于继承和不存在的属性,返回undefined

Object.getOwnPropertyDescriptor({},"x"); //undefined,没有这个属性

Object.getOwnPropertyDescriptor({},"toString"); //undefined,继承属性

想要设置属性的特性,或者想让新建属性具有某种特性,则需要调用Object.defineProperty(),传入要修改的对象、要创建或修改的属性的名称以及属性描述符对象。

传入Object.defineProperty()的属性描述符对象不必包含所有4个特性。对于新创建的属性来说,默认的特性值时false或undefined。对于修改已有属性来说,默认的特性值没有做任何修改。注意,这个方法要么修改已有属性要么新建自有属性,不能修改继承属性。

var o = {};

Object.defineProperty(o,"x",{value:1,writable:true,enumerable:false,configurable:true});

//属性时存在的,但不可枚举

o.x; //=>1

Object.keys(o); //=>[]

//现在对属性x做修改,让它变为只读

Object.defineProperty(o,"x",{writable:false});

//试图更改这个属性的值

o.x = 2;//操作失败但不报错,而在严格模式中抛出类型错误异常

o.x ; //=>1

6.8对象的三个属性

每一个对象都有与之相关的原型(prototype),类(class)和可扩展性(extensible attribute)。

6.8.1原型属性

要想检测一个对象是否是另一个对象的原型(或处于原型链中),请使用isPrototypeOf()方法。例如,可以通过p.isPrototypeOf(o)来检测p是否是o的原型:

var p  = {x:1};

var o = Object.create(p);

p.isPrototypeOf(o); // => true,o继承自p

Object.prototype.isPrototypeOf(o); // => true,p继承自Object.prototype

6.8.2类属性

对象的类属性(class attribute)是一个字符串,用以表示对象的类型信息。

var aa = 'sss';

var bb = 1;

console.log(Object.prototype.toString.call(aa));  // [object String]

console.log(Object.prototype.toString.call(aa)); // [object Number]

下面的classof()函数可以返回传递给它的任意对象的类:

function classof(o){

  if(o === null ) return "Null";

  if(o === undefined ) return "Undefined";

  return Object.prototype.toString.call(o).slice(8,-1);

}

6.8.3可扩展性

对象的可扩展性用以表示是否可以给对象添加新属性。所以内置对象和自定义对象都是显式可扩展的,宿主对象的可扩展性是由JavaScript引擎定义的。

Object.esExtensible():判断该对象是否可扩展

Object.preventExtensions():将对象转为不可扩展的。注意:一旦将对象转为不可扩展的,就无法再将其转回可扩展的了。preventExtensions()只影响到对象本身的可扩展性。如果给一个不可扩展的对象的原型添加属性,这个不可扩展的对象同样会继承这些新属性。

Object.seal():除了能将对象设置为不可扩展的,还可以将对象的所有自有属性设置为不可配置的。

Object.freeze()将更严格地锁定对象——“冻结”(frozen)。

6.9序列化对象

对象序列化(serialization)是指将对象的状态转换为字符串,也可将字符串还原为对象。ECMAScript 5提供了内置函数JSON.stringify()和JSON.parse()用来序列化和还原JavaScript对象。这些方法都使用JSON作为数据交换格式。

第7章 数组

数组直接量的语法允许有可选的结尾的逗号,故[,,]只有两个元素而非三个。

7.8.1 join()

Array.join()方法将数组中所有元素都转化为字符串并连接在一起,返回最后生成的字符串。可以指定一个可选的字符串在生成的字符串中来分隔数组的各个元素。如果不指定分隔符,默认使用逗号。

var a = [1,2,3];

a.join(); // => "1,2,3"

a.join(" "); // =>"1 2 3"

a.join(""); // => "123"

Array.join()方法是String.split()方法的逆向操作,后者是将字符串分割成若干块开创建一个数组。

7.8.2 reverse()

Array.reverse()方法将数组中的元素颠倒顺序,返回逆序的数组。它采取了替换,即不通过重新排列的元素创建新的数组,而是在原先的数组中重新排列它们。

var a = [1,2,3];

a.reverse().join(); // => "3,2,1"

console.log(a); // => [3,2,1]

7.8.3 sort()

Array.sort()方法将数组中的元素排序并返回排序后的数组。当不带参数调用sort()时,数组元素以字母表顺序排序,

为了按照其他方式而非字母表顺序进行数组排序,必须给sort()方法传递一个比较函数。

7.8.4 concat()

Array.concat()方法创建并返回一个新数组,它的元素包括调用concat()的原始数组的元素和concat()的每个参数。如果这些参数中的任何一个自身是数组,则连接的是数组的元素,而非数组本身。但要注意,concat()不会递归扁平化数组的数组。concat()也不会修改调用的数组。

var a = [1,2,3];

a.concat(4,5); //返回 [1,2,3,4,5]

a.concat([4,5]); // 返回[1,2,3,4,5]

a.concat([4,5],[6,7]); //返回[1,2,3,4,5,6,7]

a.concat(4,[5,[6,7]]); //返回[1,2,3,4,5,[6,7]]

7.8.5 slice()

Array.slice()方法返回指定数组的一个片段或子数组。它的两个参数分别指定了片段的开始和结束位置。返回的数组包含第一个参数指定的位置和所有到但不包含第二个参数指定的位置之间的所有数组元素。如果只指定一个参数,返回的数组将包含从开始位置到数组结尾的所有元素。如参数中出现负数,它表示相对于数组中最后一个元素的位置。

var a = [1,2,3,4,5];

a.slice(0,3); // 返回[1,2,3]

a.slice(1,-1); //返回[2,3,4]

a.slice(3); //返回[4,5]

a.slice(-3); // 返回[3,4,5]

a.slice(-3,-2); // 返回[3]

7.8.6 splice()

Array.splice()方法是在数组中插入或删除元素的通用方法。不同于slice()和 concat(),splice()会修改调用的数组。

splice()能够从数组中删除元素,插入元素到数组中或者同时完成这两种操作。在插入或删除点之后的数组元素会根据需要增加或减小它们的索引值,因此数组的其他部分仍然保持连续的。splice()的第一个参数指定了插入和(或)删除的起始位置。第二个参数指定了应该从数组中删除的元素的个数。如果省略第二个参数,从起始点开始到数组结尾的所有元素都将被删除。splice()返回一个由删除元素组成的数组,或者如果没有删除元素就返回一个空数组。

var a = [1,2,3,4,5,6,7,8];

a.splice(4); // 返回[5,6,7,8];a是[1,2,3,4]

a.splice(1,2); // 返回[2,3];a是[1,4]

a.splice(1,1); //返回[4];a是[1]

splice()的前两个参数指定了需要删除的数组元素。紧随其后的任意个数的参数指定了需要插入到数组中的元素,从第一个参数指定的位置开始插入。

var a = [1,2,3,4,5];

a.splice(2,0,"a","b");  //  返回 [ ] ,a是[1,2,"a","b",3,4,5]

var a = [1,2,"a","b",3,4,5];

a.splice(2,2,[1,2],3); // 返回["a","b"],a是[1,2,[1,2],3,3,4,5]

注意,区别于concat(),splice()会插入数组本身而非数组的元素

7.8.7 push()和pop()

push()和pop()方法允许将数组当做栈来使用。push()方法在数组的尾部添加一个或多个元素,并返回数组新的长度。pop()方法则相反:它删除数组的最后一个元素,减小数组长度并返回它删除的值。注意,两个方法都修改并替换原始数组而非生成一个修改版的新数组。

var stack = [];

stack.push(1,2); // stack :[1,2] 返回2

stack.pop(); // stack: [1]  返回2

stack.push(3); //stack : [1,3] 返回2

stack.pop(); //stack: [1] 返回3

stack.push([4,5]); //stack: [1,[4,5]] 返回2

stack.pop(); // stack: [1]  返回[4,5]

stack.pop(); // stack : [ ]  返回1

7.8.8 unshift()和shift()

unshift()和shift()方法的行为非常类似于push()和pop(),不一样的是前者是在数组的头部而非尾部进行元素的插入和删除操作。unshift()在数组的头部添加一个或多个元素,并将已存在的元素移动到更高索引的位置来获得足够的空间,最后返回数组新的长度。shift()删除数组的第一个元素并将其返回,然后把所有随后的元素下移一个位置来填补数组头部的空缺。

var a = [];

a.unshift(1); //a:[1] 返回 1

a.unshift(22); //a:[22,1] 返回2

a.shift(); // a: [1] 返回22

a.unshift(3,[4,5]); //a:[3,[4,5],1] 返回3

a.shift(); // a:[[4,5],1] 返回3

a.shift(); // a:[1] 返回[4,5]

a.shift(); // a: [] 返回1

注意,当使用多个参数调用unshift()时它的行为令人惊讶。参数是一次性插入的(就像splice()方法)而非一次一个地插入。这意味着最终的数组插入的元素的顺序和它们在参数列表中的顺序一致。而假如是一次一个地插入,它们的顺序应该是反过来的。

7.8.9 toString()和toLocalString()

[1,,2,3].toString() ; //生成"1,2,3"

["a","b","c"].toString(); //生成"a,b,c"

[1,[2,"c"]].toString(); //生成"1,2,c"

7.9 ECMAScript 5中的数组方法

7.9.1 forEach()

7.9.2 map()

7.9.3 filter()

7.9.4 every()和some()

7.9.5 reduce()和reduceRight()

7.9.6 indexOf和lastIndexOf()

第8章 函数

8.6闭包

和其他大多数现代编程语言一样,JavaScript也采用词法作用域,也就是说。函数的执行依赖于变量作用域,这个作用域是在函数定义时决定的,而不是函数调用时决定的。为了实现这种词法作用域,JavaScript函数对象的内部状态不仅包含函数的代码逻辑,还必须引用当前的作用域链。函数对象可以通过作用域链相互关联起来。函数体内部的变量都可以保存 在函数作用域内,这种特性在计算机学文献中称为“闭包”。

var scope = "global scope" ; //全局变量

function checkscope(){

    var scope = "local scope"; //局部变量

     function f() {return scope;} //在作用域中返回这个值

     return f();

}

checkscope(); // => "local scope"

对以上代码做一点改动

var scope = "global scope" ; //全局变量

function checkscope(){

    var scope = "local scope"; //局部变量

     function f() {return scope;} //在作用域中返回这个值

     return f;

}

checkscope()();

checkscope()现在仅仅返回函数内嵌套的一个函数对象,而不是直接返回结果。在定义函数的作用域外面,调用这个嵌套的函数(包含最后一行代码的最后一对圆括号)会发生什么事情呢?

回想一下词法作用域的基本规则:JavaScript函数的执行用到了作用域链,这个作用域链是函数定义的时候创建的。嵌套的函数f()定义在这个作用域里,其中的变量scope一定是局部变量,不管在何时何地执行函数f(),这种绑定在执行f()时依然有效。因此最后一行代码返回“local scope”,而不是“global scope”。简言之,闭包的这个特性强大到让人吃惊:它们可以捕捉到局部变量(和参数),并一直保存下来,看起来像这些变量绑定到了在其中定义它们的外部函数。

实现闭包:函数定义时的作用域链到函数执行时依然有效。如果函数定义了嵌套的函数,并将它作为返回值返回或者存储在某处的属性里,这时就会有一个外部引用指向这个嵌套的函数。它就不会被当做垃圾回收,并且它所指向的变量绑定对象也不会被当做垃圾回收。

function counter(){

   var n = 0;

   return {

        count : function() { return n++; },

        reset : function() { n =0; }

   };

}

var c = counter() , d = counter(); //创建两个计数器

c.count(); // => 0

d.count(); // => 0,它们互不干扰

c.reset(); // reset()和count()方法共享状态

c.count(); // => 0,因为我们重置了c

d.count(); // =>1,而没有重置d

counter()函数返回了一个“计数器”对象,这个对象包含两个方法:count()返回下一个整数,reset()将计数器重置为内部状态。首先要理解,这两个方法都可以访问私有变量n。再者,每次调用counter()都会创建一个新的作用域链和一个新的私有变量。因此,如果调用counter()两次,则会得到两个计数器对象,而且彼此包含不同的私有变量,调用其中一个计数器对象的count()或reset()不会影响到另外一个对象。

在同一个作用域链中定义两个闭包,这两个闭包共享同样的私有变量或变量。这是一种非常重要的技术,但还是要特别小心那些不希望共享的变量往往不经意共享给了其他的闭包,了解这一点也很重要。看一下下面这段代码:

//这个函数返回一个总是返回v的函数

function constfunc(v) { return function() { return v; };}

//创建一个数组来存储常量的函数

var funcs = [ ];

for (var i = 0; i<10; i++) funcs [i] = constfunc(i);

//在第5个位置的元素所表示的函数返回值为5

funcs[5](); // => 5

这段代码利用循环创建了很多个闭包,当写类似这中代码的时候往往会犯一个错误:那就是试图将循环代码移入定义这个闭包的函数之内,看一下这段代码:

//返回一个函数组成的数组,它们的返回值时0-9

function constfuncs(){

   var funcs = [ ];

   for(var i = 0;i<10;i++)

        funcs[i] = function() {return i;};

   return funs;

}

var funcs = constfuncs();

funcs[5](); //返回值是什么?

上面这段代码创建了10个闭包,并将它们存储到一个数组中。这些闭包都是在同一个函数调用中定义的,因此它们可以共享变量i。当constfuncs()返回时,变量i的值是10,所有的闭包都共享这一个值,因此,数组中的函数的返回值都是同一个值,这不是我们想要的结果。关联到闭包的作用域链都是“活动的”,记住这一点非常重要。嵌套的函数将不会讲作用域内的私有成员复制一份,也不会对所绑定的变量生成静态快照。

8.7.3 call()方法和apply()方法

我们可以将call()和apply()看做是某个对象的方法,通过调用方法的形式来间接调用函数。call()和apply()的第一个实参是要调用函数的母对象,它是调用上下文,在函数体内通过this来获得对它的引用。要想以对象o的方法来调用函数f(),可以这样使用call()和apply():

f.call(o);

f.apply(o);

对于call()来说,第一个调用上下文实参之后的所有实参就是要传入待调用函数的值。比如,以对象o的方法的形式调用函数f(),并传入两个参数,可以使用这样的代码:

f.call(o,1,2);

apply()方法和call()类似,但传入实参的形式和call()有所不同,它的实参都放入一个数组当中:

f.apply(o,[1,2]);

8.7.5 toString()方法

大多数的toString()方法的实现都返回函数的完整源码。内置函数往往返回一个类似"[native code]"的字符串作为函数体。

8.7.6 Function()构造函数

var f = new Function("x","y","return x*y;");

这一行代码创建一个新的函数,这个函数和通过下面代码定义的函数几乎等价:

var f = function(x,y) {return x*y;}

Function()构造函数可以传入任意数量的字符串实参,最后一个实参所表示的文本就是函数体;它可以包含任意的JavaScript语句,没两条语句之间用分号分隔。传入构造函数的其他所有的实参字符串是指定函数的形参名字的字符串。如果定义的函数不包含任何参数,只须给构造函数简单地传入一个字符串——函数体——即可。

注意,Function()构造函数并不需要通过传入实参以指定函数名。就像函数直接量一样,Function()构造函数创建一个匿名函数。

Function 构造函数所创建的函数并不是使用词法作用域,相反,函数体代码的编译总是会在顶层函数执行:

var scope = "global";

function constructFunction(){

  var scope = "local";

   return new Function("return scope"); //无法捕获局部作用域

}

//这一行代码返回global,因为通过Function()构造函数

//所返回的函数使用的不是局部作用域

constructFunction()(); // => "global"

转载请注明原文地址: https://www.6miu.com/read-71529.html

最新回复(0)