当前位置: 首页 > news >正文

网站开发学生职业规划wordpress国内几大主题

网站开发学生职业规划,wordpress国内几大主题,wordpress 调取菜单,做网站交易装备可以么函数是ECMAScript中最有意思的部分之一#xff0c;这主要是因为函数实际上是对象。每个函数都是Function 类型的实例#xff0c;而 Function 也有属性和方法#xff0c;跟其他引用类型一样。因为函数是对象#xff0c;所以函数名就是 指向函数对象的指针#xff0c;而且不…函数是ECMAScript中最有意思的部分之一这主要是因为函数实际上是对象。每个函数都是Function 类型的实例而 Function 也有属性和方法跟其他引用类型一样。因为函数是对象所以函数名就是 指向函数对象的指针而且不一定与函数本身紧密绑定。函数通常以函数声明的方式定义比如 function sum (num1, num2) { return num1 num2; } 注意函数定义最后没有加分号。 另一种定义函数的语法是函数表达式。函数表达式与函数声明几乎是等价的 let sum function(num1, num2) { return num1 num2; }; 这里代码定义了一个变量 sum 并将其初始化为一个函数。注意 function 关键字后面没有名称因为不需要。这个函数可以通过变量 sum 来引用。注意这里的函数末尾是有分号的与任何变量初始化语句一样。 还有一种定义函数的方式与函数表达式很像叫作“箭头函数”arrow function如下所示 let sum (num1, num2) { return num1 num2; }; 最后一种定义函数的方式是使用 Function 构造函数。这个构造函数接收任意多个字符串参数最后一个参数始终会被当成函数体而之前的参数都是新函数的参数。来看下面的例子 let sum new Function(num1, num2, return num1 num2); // 不推荐我们不推荐使用这种语法来定义函数因为这段代码会被解释两次第一次是将它当作常规ECMAScript 代码第二次是解释传给构造函数的字符串。这显然会影响性能。不过把函数想象为对象把函数名想象为指针是很重要的。而上面这种语法很好地诠释了这些概念。 1. 箭头函数 ECMAScript 6 新增了使用胖箭头语法定义函数表达式的能力。很大程度上箭头函数实例化的函数对象与正式的函数表达式创建的函数对象行为是相同的。任何可以使用函数表达式的地方都 可以使用箭头函数 let arrowSum (a, b) { return a b; }; let functionExpressionSum function(a, b) { return a b; }; console.log(arrowSum(5, 8)); // 13 console.log(functionExpressionSum(5, 8)); // 13箭头函数简洁的语法非常适合嵌入函数的场景 let ints [1, 2, 3]; console.log(ints.map(function(i) { return i 1; })); // [2, 3, 4] console.log(ints.map((i) { return i 1 })); // [2, 3, 4]如果只有一个参数那也可以不用括号。只有没有参数或者多个参数的情况下才需要使用括号 // 以下两种写法都有效 let double (x) { return 2 * x; }; let triple x { return 3 * x; }; // 没有参数需要括号 let getRandom () { return Math.random(); }; // 多个参数需要括号 let sum (a, b) { return a b; }; // 无效的写法 let multiply a, b { return a * b; };箭头函数也可以不用大括号但这样会改变函数的行为。使用大括号就说明包含“函数体”可以在一个函数中包含多条语句跟常规的函数一样。如果不使用大括号那么箭头后面就只能有一行代码 比如一个赋值操作或者一个表达式。而且省略大括号会隐式返回这行代码的值 // 以下两种写法都有效而且返回相应的值 let double (x) { return 2 * x; }; let triple (x) 3 * x; // 可以赋值 let value {}; let setName (x) x.name Matt; setName(value); console.log(value.name); // Matt // 无效的写法 let multiply (a, b) return a * b;箭头函数虽然语法简洁但也有很多场合不适用。箭头函数不能使用 arguments、super 和new.target也不能用作构造函数。此外箭头函数也没有 prototype 属性。 2. 函数名 因为函数名就是指向函数的指针所以它们跟其他包含对象指针的变量具有相同的行为。这意味着 一个函数可以有多个名称如下所示 function sum(num1, num2) { return num1 num2; } console.log(sum(10, 10)); // 20 let anotherSum sum; console.log(anotherSum(10, 10)); // 20 sum null; console.log(anotherSum(10, 10)); // 20注意使用不带括号的函数名会访问函数指针而不会执行函数。 ECMAScript 6 的所有函数对象都会暴露一个只读的 name 属性其中包含关于函数的信息。多数情 况下这个属性中保存的就是一个函数标识符或者说是一个字符串化的变量名。即使函数没有名称也会如实显示成空字符串。如果它是使用 Function 构造函数创建的则会标识成anonymous function foo() {} let bar function() {}; let baz () {}; console.log(foo.name); // foo console.log(bar.name); // bar console.log(baz.name); // baz console.log((() {}).name); //空字符串 console.log((new Function()).name); // anonymous如果函数是一个获取函数、设置函数或者使用 bind()实例化那么标识符前面会加上一个前缀 function foo() {} console.log(foo.bind(null).name); // bound foo let dog { years: 1, get age() { return this.years; }, set age(newAge) { this.years newAge; } } let propertyDescriptor Object.getOwnPropertyDescriptor(dog, age); console.log(propertyDescriptor.get.name); // get age console.log(propertyDescriptor.set.name); // set age3. 理解参数 ECMAScript 函数的参数跟大多数其他语言不同。ECMAScript 函数既不关心传入的参数个数也不 关心这些参数的数据类型。定义函数时要接收两个参数并不意味着调用时就传两个参数。你可以传一个、三个甚至一个也不传解释器都不会报错。 之所以会这样主要是因为 ECMAScript 函数的参数在内部表现为一个数组。函数被调用时总会接收一个数组但函数并不关心这个数组中包含什么。如果数组中什么也没有那没问题如果数组的元 素超出了要求那也没问题。事实上在使用 function 关键字定义非箭头函数时可以在函数内 部访问 arguments 对象从中取得传进来的每个参数值。 arguments 对象是一个类数组对象但不是 Array 的实例因此可以使用中括号语法访问其中的 元素第一个参数是 arguments[0]第二个参数是 arguments[1]。而要确定传进来多少个参数 可以访问 arguments.length 属性。 在下面的例子中sayHi()函数的第一个参数叫 name function sayHi(name, message) { console.log(Hello name , message); }可以通过 arguments[0]取得相同的参数值。因此把函数重写成不声明参数也可以 function sayHi() { console.log(Hello arguments[0] , arguments[1]); }ECMAScript 函数的参数只是为了方便才写出来的并不是必须写出来的。与其他语言不同在 ECMAScript 中的命名参数不会创建让之后的调用必须匹配的函数签名。这是因为根本不存在验证命名参数的机制。 也可以通过 arguments 对象的 length 属性检查传入的参数个数。下面的例子展示了在每调用一个函数时都会打印出传入的参数个数 function howManyArgs() { console.log(arguments.length); } howManyArgs(string, 45); // 2 howManyArgs(); // 0 howManyArgs(12); // 1既然如此那么开发者可以想传多少参数就传多少参数。比如 function doAdd() { if (arguments.length 1) { console.log(arguments[0] 10); } else if (arguments.length 2) { console.log(arguments[0] arguments[1]); } } doAdd(10); // 20 doAdd(30, 20); // 50这个函数 doAdd()在只传一个参数时会加 10在传两个参数时会将它们相加然后返回。因此doAdd(10)返回 20而 doAdd(30,20)返回 50。虽然不像真正的函数重载那么明确但这已经足以弥 补 ECMAScript 在这方面的缺失了。 还有一个必须理解的重要方面那就是 arguments 对象可以跟命名参数一起使用比如 function doAdd(num1, num2) { if (arguments.length 1) { console.log(num1 10); } else if (arguments.length 2) { console.log(arguments[0] num2); } }arguments 对象的另一个有意思的地方就是它的值始终会与对应的命名参数同步。来看下面的例子 function doAdd(num1, num2) { arguments[1] 10; console.log(arguments[0] num2); }这个 doAdd()函数把第二个参数的值重写为 10。因为 arguments 对象的值会自动同步到对应的命名参数所以修改 arguments[1]也会修改 num2 的值因此两者的值都是 10。但这并不意味着它们都 访问同一个内存地址它们在内存中还是分开的只不过会保持同步而已。另外还要记住一点如果只传了一个参数然后把 arguments[1]设置为某个值那么这个值并不会反映到第二个命名参数。这是因为 arguments 对象的长度是根据传入的参数个数而非定义函数时给出的命名参数个数确定的。 对于命名参数而言如果调用函数时没有传这个参数那么它的值就是 undefined。这就类似于定义了变量而没有初始化。比如如果只给 doAdd()传了一个参数那么 num2 的值就是undefined。 严格模式下arguments 会有一些变化。首先像前面那样给 arguments[1]赋值不会再影响 num2 的值。就算把 arguments[1]设置为 10num2 的值仍然还是传入的值。其次在函数中尝试重写 arguments 对象会导致语法错误。代码也不会执行。 箭头函数中的参数 如果函数是使用箭头语法定义的那么传给函数的参数将不能使用 arguments 关键字访问而只能通过定义的命名参数访问。 function foo() { console.log(arguments[0]); } foo(5); // 5 let bar () { console.log(arguments[0]); }; bar(5); // ReferenceError: arguments is not defined虽然箭头函数中没有 arguments 对象但可以在包装函数中把它提供给箭头函数 function foo() { let bar () { console.log(arguments[0]); // 5 }; bar(); } foo(5);注意 ECMAScript 中的所有参数都按值传递的。不可能按引用传递参数。如果把对象作为参数传递那么传递的值就是这个对象的引用。 4. 没有重载 ECMAScript 函数不能像传统编程那样重载。在其他语言比如 Java 中一个函数可以有两个定义只要签名接收参数的类型和数量不同就行。如前所述ECMAScript 函数没有签名因为参数是由包含零个或多个值的数组表示的。没有函数签名自然也就没有重载。 如果在 ECMAScript 中定义了两个同名函数则后定义的会覆盖先定义的。 前面也提到过可以通过检查参数的类型和数量然后分别执行不同的逻辑来模拟函数重载。 5. 默认参数值 ECMAScript5.1 及以前实现默认参数的一种常用方式就是检测某个参数是否等于 undefined如果是则意味着没有传这个参数那就给它赋一个值。 function makeKing(name) { name (typeof name ! undefined) ? name : Henry; return King ${name} VIII; } console.log(makeKing()); // King Henry VIII console.log(makeKing(Louis)); // King Louis VIIIECMAScript 6 之后就不用这么麻烦了因为它支持显式定义默认参数了。下面就是与前面代码等价的 ES6 写法只要在函数定义中的参数后面用就可以为参数赋一个默认值 function makeKing(name Henry) { return King ${name} VIII; } console.log(makeKing(Louis)); // King Louis VIII console.log(makeKing()); // King Henry VIII给参数传 undefined 相当于没有传值不过这样可以利用多个独立的默认值 function makeKing(name Henry, numerals VIII) { return King ${name} ${numerals}; } console.log(makeKing()); // King Henry VIII console.log(makeKing(Louis)); // King Louis VIII console.log(makeKing(undefined, VI)); // King Henry VI在使用默认参数时arguments 对象的值不反映参数的默认值只反映传给函数的参数。当然跟 ES5 严格模式一样修改命名参数也不会影响 arguments 对象它始终以调用函数时传入的值为准 function makeKing(name Henry) { name Louis; return King ${arguments[0]}; } console.log(makeKing()); // King undefined console.log(makeKing(Louis)); // King Louis默认参数值并不限于原始值或对象类型也可以使用调用函数返回的值 let romanNumerals [I, II, III, IV, V, VI]; let ordinality 0; function getNumerals() { // 每次调用后递增return romanNumerals[ordinality]; } function makeKing(name Henry, numerals getNumerals()) { return King ${name} ${numerals}; } console.log(makeKing()); // King Henry I console.log(makeKing(Louis, XVI)); // King Louis XVI console.log(makeKing()); // King Henry II console.log(makeKing()); // King Henry III函数的默认参数只有在函数被调用时才会求值不会在函数定义时求值。而且计算默认值的函数 只有在调用函数但未传相应参数时才会被调用。 箭头函数同样也可以这样使用默认参数只不过在只有一个参数时就必须使用括号而不能省略了 let makeKing (name Henry) King ${name}; console.log(makeKing()); // King Henry默认参数作用域与暂时性死区 因为在求值默认参数时可以定义对象也可以动态调用函数所以函数参数肯定是在某个作用域中 求值的。给多个参数定义默认值实际上跟使用 let 关键字顺序声明变量一样。来看下面的例子: function makeKing(name Henry, numerals VIII) { return King ${name} ${numerals}; } console.log(makeKing()); // King Henry VIII这里的默认参数会按照定义它们的顺序依次被初始化。 因为参数是按顺序初始化的所以后定义默认值的参数可以引用先定义的参数。 参数初始化顺序遵循“暂时性死区”规则即前面定义的参数不能引用后面定义的。像这样就会抛出错误 // 调用时不传第一个参数会报错 function makeKing(name numerals, numerals VIII) { return King ${name} ${numerals}; }参数也存在于自己的作用域中它们不能引用函数体的作用域 // 调用时不传第二个参数会报错 function makeKing(name Henry, numerals defaultNumeral) { let defaultNumeral VIII; return King ${name} ${numerals}; }6. 参数扩展与收集 ECMAScript 6 新增了扩展操作符使用它可以非常简洁地操作和组合集合数据。扩展操作符最有用 的场景就是函数定义中的参数列表在这里它可以充分利用这门语言的弱类型及参数长度可变的点。 扩展操作符既可以用于调用函数时传参也可以用于定义函数参数。 6.1 扩展参数 在给函数传参时有时候可能不需要传一个数组而是要分别传入数组的元素。 假设有如下函数定义它会将所有传入的参数累加起来 let values [1, 2, 3, 4]; function getSum() { let sum 0; for (let i 0; i arguments.length; i) { sum arguments[i]; } return sum; }如果不使用扩展操作符想把定义在这个函数这面的数组拆分那么就得求助于 apply()方法 console.log(getSum.apply(null, values)); // 10但在 ECMAScript 6 中可以通过扩展操作符极为简洁地实现这种操作。对可迭代对象应用扩展操作符并将其作为一个参数传入可以将可迭代对象拆分并将迭代返回的每个值单独传入。 比如使用扩展操作符可以将前面例子中的数组像这样直接传给函数 console.log(getSum(...values)); // 10因为数组的长度已知所以在使用扩展操作符传参的时候并不妨碍在其前面或后面再传其他的值 包括使用扩展操作符传其他参数 console.log(getSum(-1, ...values)); // 9 console.log(getSum(...values, 5)); // 15 console.log(getSum(-1, ...values, 5)); // 14 console.log(getSum(...values, ...[5,6,7])); // 28对函数中的 arguments 对象而言它并不知道扩展操作符的存在而是按照调用函数时传入的参数接收每一个值 let values [1,2,3,4] function countArguments() { console.log(arguments.length); } countArguments(-1, ...values); // 5 countArguments(...values, 5); // 5 countArguments(-1, ...values, 5); // 6 countArguments(...values, ...[5,6,7]); // 7arguments 对象只是消费扩展操作符的一种方式。在普通函数和箭头函数中也可以将扩展操作符用于命名参数当然同时也可以使用默认参数 function getProduct(a, b, c 1) { return a * b * c; } let getSum (a, b, c 0) { return a b c; } console.log(getProduct(...[1,2])); // 2 console.log(getProduct(...[1,2,3])); // 6 console.log(getProduct(...[1,2,3,4])); // 6 console.log(getSum(...[0,1])); // 1 console.log(getSum(...[0,1,2])); // 3 console.log(getSum(...[0,1,2,3])); // 36.2 收集参数 在构思函数定义时可以使用扩展操作符把不同长度的独立参数组合为一个数组。这有点类似arguments 对象的构造机制只不过收集参数的结果会得到一个 Array 实例。 function getSum(...values) { // 顺序累加 values 中的所有值// 初始值的总和为 0 return values.reduce((x, y) x y, 0); } console.log(getSum(1,2,3)); // 6收集参数的前面如果还有命名参数则只会收集其余的参数如果没有则会得到空数组。因为收集 参数的结果可变所以只能把它作为最后一个参数 // 不可以 function getProduct(...values, lastValue) {} // 可以 function ignoreFirst(firstValue, ...values) { console.log(values); } ignoreFirst(); // [] ignoreFirst(1); // [] ignoreFirst(1,2); // [2] ignoreFirst(1,2,3); // [2, 3]箭头函数虽然不支持 arguments 对象但支持收集参数的定义方式因此也可以实现与使用arguments 一样的逻辑 let getSum (...values) { return values.reduce((x, y) x y, 0); } console.log(getSum(1,2,3)); // 6另外使用收集参数并不影响 arguments 对象它仍然反映调用时传给函数的参数 function getSum(...values) { console.log(arguments.length); // 3 console.log(arguments); // [1, 2, 3] console.log(values); // [1, 2, 3] } console.log(getSum(1,2,3));7. 函数声明与函数表达式 到现在一直没有把函数声明和函数表达式区分得很清楚。事实上JavaScript 引擎在加载数据时对它们是区别对待的。JavaScript 引擎在任何代码执行之前会先读取函数声明并在执行上下文中生成函数定义。而函数表达式必须等到代码执行到它那一行才会在执行上下文中生成函数定义。来看 下面的例子 // 没问题 console.log(sum(10, 10)); function sum(num1, num2) { return num1 num2; }以上代码可以正常运行因为函数声明会在任何代码执行之前先被读取并添加到执行上下文。这个过程叫作函数声明提升function declaration hoisting。在执行代码时JavaScript 引擎会先执行一遍扫描把发现的函数声明提升到源代码树的顶部。因此即使函数定义出现在调用它们的代码之后引擎也会把函数声明提升到顶部。如果把前面代码中的函数声明改为等价的函数表达式那么执行的时候就会出错 // 会出错 console.log(sum(10, 10)); let sum function(num1, num2) { return num1 num2; };这并不是因为使用 let 而导致的使用 var 关键字也会碰到同样的问题,除了函数什么时候真正有定义这个区别之外这两种语法是等价的。 8. 函数作为值 因为函数名在 ECMAScript 中就是变量所以函数可以用在任何可以使用变量的地方。这意味着不仅可以把函数作为参数传给另一个函数而且还可以在一个函数中返回另一个函数。来看下面的例子 function callSomeFunction(someFunction, someArgument) { return someFunction(someArgument); }这个函数接收两个参数。第一个参数应该是一个函数第二个参数应该是要传给这个函数的值。任 何函数都可以像下面这样作为参数传递 function add10(num) { return num 10; } let result1 callSomeFunction(add10, 10); console.log(result1); // 20 function getGreeting(name) { return Hello, name; } let result2 callSomeFunction(getGreeting, Nicholas); console.log(result2); // Hello, Nicholas要注意的是如果是访问函数而不是调用函数那就必须不带括号所以传给 callSomeFunction()的必须是 add10 和 getGreeting而不能是它们的执行结果。 从一个函数中返回另一个函数也是可以的而且非常有用。例如假设有一个包含对象的数组而我们想按照任意对象属性对数组进行排序。为此可以定义一个 sort()方法需要的比较函数它接收两个参数即要比较的值。但这个比较函数还需要想办法确定根据哪个属性来排序。这个问题可以通过 定义一个根据属性名来创建比较函数的函数来解决。比如 function createComparisonFunction(propertyName) { return function(object1, object2) { let value1 object1[propertyName]; let value2 object2[propertyName]; if (value1 value2) { return -1; } else if (value1 value2) { return 1; } else { return 0; } }; }这个函数的语法乍一看比较复杂但实际上就是在一个函数中返回另一个函数注意那个 return操作符。内部函数可以访问 propertyName 参数并通过中括号语法取得要比较的对象的相应属性值。 取得属性值以后再按照 sort()方法的需要返回比较值就行了。这个函数可以像下面这样使用 let data [ {name: Zachary, age: 28}, {name: Nicholas, age: 29} ]; data.sort(createComparisonFunction(name)); console.log(data[0].name); // Nicholas data.sort(createComparisonFunction(age)); console.log(data[0].name); // Zachary9. 函数内部 ECMAScript 5 中函数内部存在两个特殊的对象arguments 和 this。ECMAScript 6 又新增 了 new.target 属性。 9.1 arguments arguments 对象前面讨论过多次了它是一个类数组对象包含调用函数时传入的所有参数。这个对象只有以 function 关键字定义函数相对于使用箭头语法创建函数时才会有。虽然主要用于包含函数参数但 arguments 对象其实还有一个 callee 属性是一个指向 arguments 对象所在函数的指针。来看下面这个经典的阶乘函数 function factorial(num) { if (num 1) { return 1; } else { return num * factorial(num - 1); } }阶乘函数一般定义成递归调用的就像上面这个例子一样。只要给函数一个名称而且这个名称不会变这样定义就没有问题。但是这个函数要正确执行就必须保证函数名是 factorial从而导致了紧密耦合。使用 arguments.callee 就可以让函数逻辑与函数名解耦 function factorial(num) { if (num 1) { return 1; } else { return num * arguments.callee(num - 1); } }9.2 this 另一个特殊的对象是 this它在标准函数和箭头函数中有不同的行为。 在标准函数中this 引用的是把函数当成方法调用的上下文对象这时候通常称其为 this 值在网页的全局上下文中调用函数时this 指向 windows。来看下面的例子 window.color red; let o { color: blue }; function sayColor() { console.log(this.color); } sayColor(); // red o.sayColor sayColor; o.sayColor(); // blue定义在全局上下文中的函数 sayColor()引用了 this 对象。这个 this 到底引用哪个对象必须到函数被调用时才能确定。因此这个值在代码执行的过程中可能会变。 在箭头函数中this引用的是定义箭头函数的上下文。下面的例子演示了这一点。在对sayColor() 的两次调用中this 引用的都是 window 对象因为这个箭头函数是在 window 上下文中定义的 window.color red; let o { color: blue }; let sayColor () console.log(this.color); sayColor(); // red o.sayColor sayColor; o.sayColor(); // red在事件回调或定时回调中调用某个函数时this 值指向的并非想要的对象。此时将回调函数写成箭头函数就可以解决问题。这是因为箭头函数中的 this 会保留定义该函数时的上下文 function King() { this.royaltyName Henry; // this 引用 King 的实例setTimeout(() console.log(this.royaltyName), 1000); } function Queen() { this.royaltyName Elizabeth; // this 引用 window 对象setTimeout(function() { console.log(this.royaltyName); }, 1000); } new King(); // Henry new Queen(); // undefined注意 函数名只是保存指针的变量。因此全局定义的 sayColor()函数和 o.sayColor()是同一个函数只不过执行的上下文不同。 9.3 caller ECMAScript 5 也会给函数对象上添加一个属性caller。虽然 ECMAScript 3 中并没有定义但所有浏览器除了早期版本的 Opera 都支持这个属性。这个属性引用的是调用当前函数的函数或者如果是 在全局作用域中调用的则为 null。比如 function outer() { inner(); } function inner() { console.log(inner.caller); } outer();以上代码会显示 outer()函数的源代码。这是因为 ourter()调用了 inner()inner.caller指向 outer()。如果要降低耦合度则可以通过 arguments.callee.caller 来引用同样的值javascript function outer() { inner(); } function inner() { console.log(arguments.callee.caller); } outer();在严格模式下访问 arguments.callee 会报错。ECMAScript 5 也定义了 arguments.caller但在严格模式下访问它会报错在非严格模式下则始终是 undefined。这是为了分清 arguments.caller和函数的 caller 而故意为之的。而作为对这门语言的安全防护这些改动也让第三方代码无法检测同一上下文中运行的其他代码。 严格模式下还有一个限制就是不能给函数的 caller 属性赋值否则会导致错误。 9.4 new.target ECMAScript 中的函数始终可以作为构造函数实例化一个新对象也可以作为普通函数被调用。 ECMAScript 6 新增了检测函数是否使用 new 关键字调用的 new.target 属性。如果函数是正常调用的则 new.target 的值是 undefined如果是使用 new 关键字调用的则 new.target 将引用被调用的 构造函数。 function King() { if (!new.target) { throw King must be instantiated using new } console.log(King instantiated using new); } new King(); // King instantiated using new King(); // Error: King must be instantiated using new10. 函数属性与方法 ECMAScript 中的函数是对象因此有属性和方法。每个函数都有两个属性length和 prototype。其中length 属性保存函数定义的命名参数的个数如下例所示 function sayName(name) { console.log(name); } function sum(num1, num2) { return num1 num2; } function sayHi() { console.log(hi); } console.log(sayName.length); // 1 console.log(sum.length); // 2 console.log(sayHi.length); // 0prototype 属性也许是 ECMAScript 核心中最有趣的部分。prototype 是保存引用类型所有实例方法的地方这意味着 toString()、valueOf()等方法实际上都保存在 prototype 上进而由所有实 例共享。 这个属性在自定义类型时特别重要。在 ECMAScript 5中prototype 属性是不可枚举的因此使用 for-in 循环不会返回这个属性。 函数还有两个方法apply()和 call()。这两个方法都会以指定的 this 值来调用函数即会设置调用函数时函数体内 this 对象的值。apply()方法接收两个参数函数内 this 的值和一个参数数组。第二个参数可以是 Array 的实例但也可以是 arguments 对象。来看下面的例子 function sum(num1, num2) { return num1 num2; } function callSum1(num1, num2) { return sum.apply(this, arguments); // 传入 arguments 对象 } function callSum2(num1, num2) { return sum.apply(this, [num1, num2]); // 传入数组 } console.log(callSum1(10, 10)); // 20 console.log(callSum2(10, 10)); // 20注意 在严格模式下调用函数时如果没有指定上下文对象则 this 值不会指向 window。 除非使用 apply()或 call()把函数指定给一个对象否则 this 的值会变成 undefined。 call()方法与 apply()的作用一样只是传参的形式不同。第一个参数跟 apply()一样也是 this值而剩下的要传给被调用函数的参数则是逐个传递的。换句话说通过 call()向函数传参时必须将参数一个一个地列出来比如 function sum(num1, num2) { return num1 num2; } function callSum(num1, num2) { return sum.call(this, num1, num2); } console.log(callSum(10, 10)); // 20到底是使用 apply()还是 call()完全取决于怎么给要调用的函数传参更方便。如果想直接传arguments对象或者一个数组那就用 apply()否则就用 call()。当然如果不用给被调用的函数传参则使用哪个方法都一样。 apply()和 call()真正强大的地方并不是给函数传参而是控制函数调用上下文即函数体内 this值的能力。考虑下面的例子 window.color red; let o { color: blue }; function sayColor() { console.log(this.color); } sayColor(); // red sayColor.call(this); // red sayColor.call(window); // red sayColor.call(o); // blue使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域这样对象可以不用关心方法。在前面例子最初的版本中为切换上下文需要先把 sayColor()直接赋值为 o 的属性然后再调用。而在这个修改后的版本中就不需要这一步操作了。 ECMAScript 5 出于同样的目的定义了一个新方法bind()。bind()方法会创建一个新的函数实例其 this 值会被绑定到传给 bind()的对象。比如 window.color red; var o { color: blue }; function sayColor() { console.log(this.color); } let objectSayColor sayColor.bind(o); objectSayColor(); // blue这里在 sayColor()上调用 bind()并传入对象 o 创建了一个新函数 objectSayColor()。objectSayColor()中的 this 值被设置为 o因此直接调用这个函数即使是在全局作用域中调用 也会返回字符串blue。 对函数而言继承的方法 toLocaleString()和 toString()始终返回函数的代码。返回代码的具体格式因浏览器而异。有的返回源代码包含注释而有的只返回代码的内部形式会删除注释甚至代码可能被解释器修改过。由于这些差异因此不能在重要功能中依赖这些方法返回的值而只应在调试中使用它们。继承的方法 valueOf()返回函数本身。 11. 函数表达式 函数表达式虽然更强大但也更容易让人迷惑。我们知道定义函数有两种方式函数声明和函数 表达式。函数声明是这样的 function functionName(arg0, arg1, arg2) { // 函数体 }函数声明的关键特点是函数声明提升即函数声明会在代码执行之前获得定义。这意味着函数声明可以出现在调用它的代码之后. 第二种创建函数的方式就是函数表达式。函数表达式有几种不同的形式最常见的是这样的 let functionName function(arg0, arg1, arg2) { // 函数体 };函数表达式看起来就像一个普通的变量定义和赋值即创建一个函数再把它赋值给一个变量functionName。这样创建的函数叫作匿名函数anonymous funtion因为 function 关键字后面没有标识符。匿名函数有也时候也被称为兰姆达函数lambda。未赋值给其他变量的匿名函数的 name 属性是空字符串。 函数表达式跟 JavaScript 中的其他表达式一样需要先赋值再使用。下面的例子会导致错误 sayHi(); // Error! function doesnt exist yet let sayHi function() { console.log(Hi!); };理解函数声明与函数表达式之间的区别关键是理解提升。比如以下代码的执行结果可能会出乎 意料 // 千万别这样做 if (condition) { function sayHi() { console.log(Hi!); } } else { function sayHi() { console.log(Yo!); } }这段代码看起来很正常就是如果 condition 为 true则使用第一个 sayHi()定义否则就使用第二个。事实上这种写法在 ECAMScript 中不是有效的语法。JavaScript 引擎会尝试将其纠正为适当的声明。问题在于浏览器纠正这个问题的方式并不一致。多数浏览器会忽略 condition 直接返回第二个声明。Firefox 会在 condition 为 true 时返回第一个声明。这种写法很危险不要使用。不过如果把上面的函数声明换成函数表达式就没问题了 // 没问题 let sayHi; if (condition) { sayHi function() { console.log(Hi!); }; } else { sayHi function() { console.log(Yo!); }; }这个例子可以如预期一样根据 condition 的值为变量 sayHi 赋予相应的函数。 创建函数并赋值给变量的能力也可以用于在一个函数中把另一个函数当作值返回 function createComparisonFunction(propertyName) { return function(object1, object2) { let value1 object1[propertyName]; let value2 object2[propertyName]; if (value1 value2) { return -1; } else if (value1 value2) { return 1; } else { return 0; } }; }12. 递归 递归函数通常的形式是一个函数通过名称调用自己如下面的例子所示 function factorial(num) { if (num 1) { return 1; } else { return num * factorial(num - 1); } }这是经典的递归阶乘函数。虽然这样写是可以的但如果把这个函数赋值给其他变量就会出问题 let anotherFactorial factorial; factorial null; console.log(anotherFactorial(4)); // 报错这里把 factorial()函数保存在了另一个变量 anotherFactorial 中然后将 factorial 设置为 null于是只保留了一个对原始函数的引用。而在调用 anotherFactorial()时要递归调用factorial()但因为它已经不是函数了所以会出错。在写递归函数时使用 arguments.callee 可以避免这个问题。 arguments.callee 就是一个指向正在执行的函数的指针因此可以在函数内部递归调用如下所示 function factorial(num) { if (num 1) { return 1; } else { return num * arguments.callee(num - 1); } }不过在严格模式下运行的代码是不能访问 arguments.callee 的因为访问会出错。此时可以使用命名函数表达式named function expression达到目的。比如 const factorial (function f(num) { if (num 1) { return 1; } else { return num * f(num - 1); } });这里创建了一个命名函数表达式 f()然后将它赋值给了变量 factorial。即使把函数赋值给另一个变量函数表达式的名称 f 也不变因此递归调用不会有问题。这个模式在严格模式和非严格模式下都可以使用。 13. 尾调用优化 ECMAScript 6 规范新增了一项内存管理优化机制让 JavaScript 引擎在满足条件时可以重用栈帧。 具体来说这项优化非常适合“尾调用”即外部函数的返回值是一个内部函数的返回值。比如 function outerFunction() { return innerFunction(); // 尾调用 }在 ES6 优化之前执行这个例子会在内存中发生如下操作。 执行到 outerFunction 函数体第一个栈帧被推到栈上。执行 outerFunction 函数体到 return 语句。计算返回值必须先计算 innerFunction。执行到 innerFunction 函数体第二个栈帧被推到栈上。执行 innerFunction 函数体计算其返回值。将返回值传回 outerFunction然后 outerFunction 再返回值。将栈帧弹出栈外。 在 ES6 优化之后执行这个例子会在内存中发生如下操作。 执行到 outerFunction 函数体第一个栈帧被推到栈上。执行 outerFunction 函数体到达 return 语句。为求值返回语句必须先求值 innerFunction。引擎发现把第一个栈帧弹出栈外也没问题因为 innerFunction 的返回值也是 outerFunction的返回值。弹出 outerFunction 的栈帧。执行到 innerFunction 函数体栈帧被推到栈上。执行 innerFunction 函数体计算其返回值。将 innerFunction 的栈帧弹出栈外。 很明显第一种情况下每多调用一次嵌套函数就会多增加一个栈帧。而第二种情况下无论调用多少次嵌套函数都只有一个栈帧。这就是 ES6 尾调用优化的关键如果函数的逻辑允许基于尾调用将其 销毁则引擎就会那么做。 注意 现在还没有办法测试尾调用优化是否起作用。不过因为这是 ES6 规范所规定的兼容的浏览器实现都能保证在代码满足条件的情况下应用这个优化。 13.1 尾调用优化的条件 尾调用优化的条件就是确定外部栈帧真的没有必要存在了。涉及的条件如下 代码在严格模式下执行外部函数的返回值是对尾调用函数的调用尾调用函数返回后不需要执行额外的逻辑尾调用函数不是引用外部函数作用域中自由变量的闭包。 下面展示了几个违反上述条件的函数因此都不符号尾调用优化的要求 use strict; // 无优化尾调用没有返回 function outerFunction() { innerFunction(); } // 无优化尾调用没有直接返回 function outerFunction() { let innerFunctionResult innerFunction(); return innerFunctionResult; } // 无优化尾调用返回后必须转型为字符串 function outerFunction() { return innerFunction().toString(); } // 无优化尾调用是一个闭包 function outerFunction() { let foo bar; function innerFunction() { return foo; } return innerFunction(); } 下面是几个符合尾调用优化条件的例子 use strict; // 有优化栈帧销毁前执行参数计算 function outerFunction(a, b) { return innerFunction(a b); } // 有优化初始返回值不涉及栈帧 function outerFunction(a, b) { if (a b) { return a; } return innerFunction(a b); } // 有优化两个内部函数都在尾部 function outerFunction(condition) { return condition ? innerFunctionA() : innerFunctionB(); }差异化尾调用和递归尾调用是容易让人混淆的地方。无论是递归尾调用还是非递归尾调用都可以应用优化。引擎并不区分尾调用中调用的是函数自身还是其他函数。不过这个优化在递归场景下的效 果是最明显的因为递归代码最容易在栈内存中迅速产生大量栈帧。 注意: 之所以要求严格模式主要因为在非严格模式下函数调用中允许使用 f.arguments和 f.caller而它们都会引用外部函数的栈帧。显然这意味着不能应用优化了。因此尾调用优化要求必须在严格模式下有效以防止引用这些属性。 13.2 尾调用优化的代码 可以通过把简单的递归函数转换为待优化的代码来加深对尾调用优化的理解。下面是一个通过递归 计算斐波纳契数列的函数 function fib(n) { if (n 2) { return n; } return fib(n - 1) fib(n - 2); } console.log(fib(0)); // 0 console.log(fib(1)); // 1 console.log(fib(2)); // 1 console.log(fib(3)); // 2 console.log(fib(4)); // 3 console.log(fib(5)); // 5 console.log(fib(6)); // 8显然这个函数不符合尾调用优化的条件因为返回语句中有一个相加的操作。 当然解决这个问题也有不同的策略比如把递归改写成迭代循环形式。不过也可以保持递归实现但将其重构为满足优化条件的形式。为此可以使用两个嵌套的函数外部函数作为基础框架内部函数执行递归 use strict; // 基础框架 function fib(n) { return fibImpl(0, 1, n); } // 执行递归 function fibImpl(a, b, n) { if (n 0) { return a; } return fibImpl(b, a b, n - 1); }14. 闭包 匿名函数经常被人误认为是闭包closure。闭包指的是那些引用了另一个函数作用域中变量的函数通常是在嵌套函数中实现的。比如下面是之前展示的 createComparisonFunction()函数 function createComparisonFunction(propertyName) { return function(object1, object2) { //引用了外部函数的变量 propertyNamelet value1 object1[propertyName]; let value2 object2[propertyName]; if (value1 value2) { return -1; } else if (value1 value2) { return 1; } else { return 0; } }; }理解作用域链创建和使用的细节对理解闭包非常重要。在调用一个函数时会为这个函数调用创建一个执行上下文并创建一个作用域链。然后用 arguments和其他命名参数来初始化这个函数的活动对象。外部函数的活动对象是内部函数作用域链上的第二个对象。这个作用域链一直向外串起了所有包含函数的活动对象直到全局执行上下文才终止。 在函数执行时要从作用域链中查找变量以便读、写值。来看下面的代码 function compare(value1, value2) { if (value1 value2) { return -1; } else if (value1 value2) { return 1; } else { return 0; } } let result compare(5, 10);这里定义的 compare()函数是在全局上下文中调用的。第一次调用 compare()时会为它创建一个包含 arguments、value1 和 value2 的活动对象这个对象是其作用域链上的第一个对象。而全局上下文的变量对象则是 compare()作用域链上的第二个对象其中包含 this、result 和 compare。 函数内部的代码在访问变量时就会使用给定的名称从作用域链中查找变量。函数执行完毕后局 部活动对象会被销毁内存中就只剩下全局作用域。不过闭包就不一样了。 在一个函数内部定义的函数会把其包含函数的活动对象添加到自己的作用域链中。因此在 createComparisonFunction()函数中匿名函数的作用域链中实际上包含createComparisonFunction()的活动对象。 let compare createComparisonFunction(name); let result compare({ name: Nicholas }, { name: Matt });上面代码执行后的结果如图 在 createComparisonFunction()返回匿名函数后它的作用域链被初始化为包含createComparisonFunction()的活动对象和全局变量对象。这样匿名函数就可以访问到createComparisonFunction()可以访问的所有变量。另一个有意思的副作用就是createComparisonFunction()的活动对象并不能在它执行完毕后销毁因为匿名函数的作用域链中仍然有对它的引用。在 createComparisonFunction()执行完毕后其执行上下文的作用域链会销毁但它的活动对象仍然会保留在内存中直到匿名函数被销毁后才会被销毁 // 创建比较函数 let compareNames createComparisonFunction(name); // 调用函数 let result compareNames({ name: Nicholas }, { name: Matt }); // 解除对函数的引用这样就可以释放内存了 compareNames null;注意 因为闭包会保留它们包含函数的作用域所以比其他函数更占用内存。过度使用闭包可能导致内存过度占用因此建议仅在十分必要时使用。 14.1 this 对象 在闭包中使用 this 会让代码变复杂。如果内部函数没有使用箭头函数定义则 this 对象会在运行时绑定到执行函数的上下文。如果在全局函数中调用则 this 在非严格模式下等于 window在严格模式下等于 undefined。如果作为某个对象的方法调用则 this 等于这个对象。匿名函数在这种情况下不会绑定到某个对象这就意味着 this 会指向 window除非在严格模式下 this 是 undefined。不过由于闭包的写法所致这个事实有时候没有那么容易看出来。来看下面的例子 window.identity The Window; let object { identity: My Object, getIdentityFunc() { return function() { return this.identity; }; } }; console.log(object.getIdentityFunc()()); // The Window为什么匿名函数没有使用其包含作用域getIdentityFunc()的 this 对象呢 每个函数在被调用时都会自动创建两个特殊变量this 和 arguments。内部函数永远不可能直接访问外部函数的这两个变量。但是如果把 this 保存到闭包可以访问的另一个变量中则是行得通的。比如 window.identity The Window; let object { identity: My Object, getIdentityFunc() { let that this; return function() { return that.identity; }; } }; console.log(object.getIdentityFunc()()); // My Object在一些特殊情况下this 值可能并不是我们所期待的值。比如下面这个修改后的例子 window.identity The Window; let object { identity: My Object, getIdentity () { return this.identity; } };getIdentity()方法就是返回 this.identity 的值。以下是几种调用 object.getIdentity()的方式及返回值 object.getIdentity(); // My Object (object.getIdentity)(); // My Object (object.getIdentity object.getIdentity)(); // The Window第一行调用 object.getIdentity()是正常调用会返回My Object因为 this.identity就是 object.identity。第二行在调用时把 object.getIdentity 放在了括号里。虽然加了括号之后看起来是对一个函数的引用但 this 值并没有变。这是因为按照规范object.getIdentity 和(object.getIdentity)是相等的。第三行执行了一次赋值然后再调用赋值后的结果。因为赋值表达式的值是函数本身this 值不再与任何对象绑定所以返回的是The Window。 一般情况下不大可能像第二行和第三行这样调用对象上的方法。但通过这个例子我们可以知道 即使语法稍有不同也可能影响 this 的值。 14.2 内存泄漏 由于 IE 在 IE9 之前对 JScript 对象和 COM 对象使用了不同的垃圾回收机制所以闭包在这些旧版本 IE 中可能会导致问题。在这些版本的 IE 中把 HTML 元素保存在某个闭包的作用域中就相当于宣布该元素不能被销毁。来看下面的例子 function assignHandler() { let element document.getElementById(someElement); element.onclick () console.log(element.id); }以上代码创建了一个闭包即 element 元素的事件处理程序。而这个处理程序又创建了一个循环引用。匿名函数引用着 assignHandler()的活动对象阻止了对element 的引用计数归零。只要这个匿名函数存在element 的引用计数就至少等于 1。也就是说内存不会被回收。其实只要这个例子稍加修改就可以避免这种情况比如 function assignHandler() { let element document.getElementById(someElement); let id element.id; element.onclick () console.log(id);element null; }在这个修改后的版本中闭包改为引用一个保存着 element.id 的变量 id从而消除了循环引用。不过光有这一步还不足以解决内存问题。因为闭包还是会引用包含函数的活动对象而其中包含element。即使闭包没有直接引用 element包含函数的活动对象上还是保存着对它的引用。因此必 须再把 element 设置为 null。这样就解除了对这个 COM 对象的引用其引用计数也会减少从而确 保其内存可以在适当的时候被回收。 15. 立即调用的函数表达式 立即调用的匿名函数又被称作立即调用的函数表达式IIFEImmediately Invoked Function Expression。它类似于函数声明但由于被包含在括号中所以会被解释为函数表达式。紧跟在第一组括号后面的第二组括号会立即调用前面的函数表达式。下面是一个简单的例子 (function() { // 块级作用域 })();使用 IIFE 可以模拟块级作用域即在一个函数表达式内部声明变量然后立即调用这个函数。这样位于函数体作用域的变量就像是在块级作用域中一样。ECMAScript 5 尚未支持块级作用域使用 IIFE模拟块级作用域是相当普遍的。比如下面的例子 // IIFE (function () { for (var i 0; i count; i) { console.log(i); } })(); console.log(i); // 抛出错误在 ECMAScript 5.1 及以前为了防止变量定义外泄IIFE 是个非常有效的方式。这样也不会导致闭包相关的内存问题因为不存在对这个匿名函数的引用。为此只要函数执行完毕其作用域链就可以被销毁。 在 ECMAScript 6 以后IIFE 就没有那么必要了因为块级作用域中的变量无须 IIFE 就可以实现同 样的隔离。下面展示了两种不同的块级作用域形式 // 内嵌块级作用域 { let i; for (i 0; i count; i) { console.log(i); } } console.log(i); // 抛出错误 // 循环的块级作用域 for (let i 0; i count; i) { console.log(i); } console.log(i); // 抛出错误说明 IIFE 用途的一个实际的例子就是可以用它锁定参数值。比如 let divs document.querySelectorAll(div); // 达不到目的 for (var i 0; i divs.length; i) { divs[i].addEventListener(click, function() { console.log(i); }); }这里使用 var 关键字声明了循环迭代变量 i但这个变量并不会被限制在 for 循环的块级作用域内。因此渲染到页面上之后点击每个 都会弹出元素总数。这是因为在执行单击处理程序时迭代变 量的值是循环结束时的最终值即元素的个数。而且这个变量 i 存在于循环体外部随时可以访问。 以前为了实现点击第几个 就显示相应的索引值需要借助 IIFE 来执行一个函数表达式传入每次循环的当前索引从而“锁定”点击时应该显示的索引值 let divs document.querySelectorAll(div); for (var i 0; i divs.length; i) { divs[i].addEventListener(click, (function(frozenCounter) {return function() { console.log(frozenCounter); }; })(i)); }而使用 ECMAScript 块级作用域变量就不用这么大动干戈了 let divs document.querySelectorAll(div); //此处使用let定义变量 for (let i 0; i divs.length; i) { divs[i].addEventListener(click, function() { console.log(i); }); }但要注意如果把变量声明拿到 for 循环外部那就不行了。下面这种写法会碰到跟在循环中使用 var i 0 同样的问题 let divs document.querySelectorAll(div); // 达不到目的 let i; for (i 0; i divs.length; i) { divs[i].addEventListener(click, function() { console.log(i); }); }16. 私有变量 严格来讲JavaScript 没有私有成员的概念所有对象属性都公有的。不过倒是有私有变量的概念。任何定义在函数或块中的变量都可以认为是私有的因为在这个函数或块的外部无法访问其中的变量。私有变量包括函数参数、局部变量以及函数内部定义的其他函数。来看下面的例子 function add(num1, num2) { let sum num1 num2; return sum; }在这个函数中函数 add()有 3 个私有变量num1、num2 和 sum。这几个变量只能在函数内部使 用不能在函数外部访问。如果这个函数中创建了一个闭包则这个闭包能通过其作用域链访问其外部的这 3 个变量。基于这一点就可以创建出能够访问私有变量的公有方法。 特权方法privileged method是能够访问函数私有变量及私有函数的公有方法。在对象上有两种方式创建特权方法。第一种是在构造函数中实现比如 function MyObject() { // 私有变量和私有函数 let privateVariable 10; function privateFunction() { return false; } // 特权方法this.publicMethod function() { privateVariable; return privateFunction(); }; }这个模式是把所有私有变量和私有函数都定义在构造函数中。然后再创建一个能够访问这些私有成员的特权方法。这样做之所以可行是因为定义在构造函数中的特权方法其实是一个闭包它具有访 问构造函数中定义的所有变量和函数的能力。 如下面的例子所示可以定义私有变量和特权方法以隐藏不能被直接修改的数据 function Person(name) { this.getName function() { return name; }; this.setName function (value) { name value; }; } let person new Person(Nicholas); console.log(person.getName()); // Nicholas person.setName(Greg); console.log(person.getName()); // Greg这段代码中的构造函数定义了两个特权方法getName()和 setName()。每个方法都可以构造函数外部调用并通过它们来读写私有的 name 变量。在 Person 构造函数外部没有别的办法访问name。 因为两个方法都定义在构造函数内部所以它们都是能够通过作用域链访问 name 的闭包。私有变量 name 对每个 Person 实例而言都是独一无二的因为每次调用构造函数都会重新创建一套变量和方法。不过这样也有个问题必须通过构造函数来实现这种隔离。正如前面所讨论的构造函数模式的缺点是每个实例都会重新创建一遍新方法。使用静态私有变量实现特权方法可以避免这个问题。 16.1 静态私有变量 特权方法也可以通过使用私有作用域定义私有变量和函数来实现。这个模式如下所示 (function() { // 私有变量和私有函数let privateVariable 10; function privateFunction() { return false; } // 构造函数MyObject function() {}; // 公有和特权方法MyObject.prototype.publicMethod function() { privateVariable; return privateFunction(); }; })();在这个模式中匿名函数表达式创建了一个包含构造函数及其方法的私有作用域。首先定义的是私有变量和私有函数然后又定义了构造函数和公有方法。公有方法定义在构造函数的原型上与典型的 原型模式一样。注意这个模式定义的构造函数没有使用函数声明使用的是函数表达式。函数声明会创建内部函数在这里并不是必需的。基于同样的原因但操作相反这里声明 MyObject 并没有使用任何关键字。因为不使用关键字声明的变量会创建在全局作用域中所以 MyObject 变成了全局变量可以在这个私有作用域外部被访问。注意在严格模式下给未声明的变量赋值会导致错误。 这个模式与前一个模式的主要区别就是私有变量和私有函数是由实例共享的。因为特权方法定义在原型上所以同样是由实例共享的。特权方法作为一个闭包始终引用着包含它的作用域。来看下面 的例子 (function() { let name ; Person function(value) { name value; }; Person.prototype.getName function() { return name; }; Person.prototype.setName function(value) { name value; }; })(); let person1 new Person(Nicholas); console.log(person1.getName()); // Nicholas person1.setName(Matt); console.log(person1.getName()); // Matt let person2 new Person(Michael); console.log(person1.getName()); // Michael console.log(person2.getName()); // Michael这里的 Person 构造函数可以访问私有变量 name跟 getName()和 setName()方法一样。使用这种模式name 变成了静态变量可供所有实例使用。这意味着在任何实例上调用 setName()修改这个变量都会影响其他实例。调用 setName()或创建新的 Person 实例都要把 name 变量设置为一个新值。 而所有实例都会返回相同的值。 像这样创建静态私有变量可以利用原型更好地重用代码只是每个实例没有了自己的私有变量。最终到底是把私有变量放在实例中还是作为静态私有变量都需要根据自己的需求来确定。 注意 使用闭包和私有变量会导致作用域链变长作用域链越长则查找变量所需的时间也越多。 16.2 模块模式 Douglas Crockford 所说的模块模式在一个单例对象上实现了相同的隔离和封装。单例对象singleton就是只有一个实例的对象。按照惯例JavaScript 是通过对象字面量来创建单例对象的如下面的例子所示 let singleton { name: value, method() { // 方法的代码} };模块模式是在单例对象基础上加以扩展使其通过作用域链来关联私有变量和特权方法。模块模式 的样板代码如下 let singleton function() { // 私有变量和私有函数let privateVariable 10; function privateFunction() { return false; } // 特权/公有方法和属性return { publicProperty: true, publicMethod() { privateVariable; return privateFunction(); } }; }();模块模式使用了匿名函数返回一个对象。在匿名函数内部首先定义私有变量和私有函数。之后创建一个要通过匿名函数返回的对象字面量。这个对象字面量中只包含可以公开访问的属性和方法。因 为这个对象定义在匿名函数内部所以它的所有公有方法都可以访问同一个作用域的私有变量和私有函数。本质上对象字面量定义了单例对象的公共接口。如果单例对象需要进行某种初始化并且需要访问私有变量时那就可以采用这个模式 let application function() { // 私有变量和私有函数 let components new Array(); // 初始化components.push(new BaseComponent()); // 公共接口return { getComponentCount() { return components.length; }, registerComponent(component) { if (typeof component object) { components.push(component); } } }; }();Web 开发中经常需要使用单例对象管理应用程序级的信息。上面这个简单的例子创建了一个application 对象用于管理组件。在创建这个对象之后内部就会创建一个私有的数组 components 然后将一个 BaseComponent 组件的新实例添加到数组中。BaseComponent 组件的代码并不重要在这里用它只是为了说明模块模式的用法。对象字面量中定义的 getComponentCount()和 registerComponent()方法都是可以访问 components 私有数组的特权方法。前一个方法返回注册组件的数量后一个方法负责注册新组件。 在模块模式中单例对象作为一个模块经过初始化可以包含某些私有的数据而这些数据又可以通过其暴露的公共方法来访问。以这种方式创建的每个单例对象都是 Object 的实例因为最终单例都由一个对象字面量来表示。不过这无关紧要因为单例对象通常是可以全局访问的而不是作为参数传给函数的所以可以避免使用 instanceof 操作符确定参数是不是对象类型的需求。 16.3 模块增强模式 另一个利用模块模式的做法是在返回对象之前先对其进行增强。这适合单例对象需要是某个特定类型的实例但又必须给它添加额外属性或方法的场景。来看下面的例子 let singleton function() { // 私有变量和私有函数let privateVariable 10; function privateFunction() { return false; } // 创建对象let object new CustomType(); // 添加特权/公有属性和方法object.publicProperty true; object.publicMethod function() { privateVariable; return privateFunction(); }; // 返回对象return object; }();如果前一节的 application 对象必须是 BaseComponent 的实例那么就可以使用下面的代码来创建它 let application function() { // 私有变量和私有函数 let components new Array(); // 初始化components.push(new BaseComponent()); // 创建局部变量保存实例let app new BaseComponent(); // 公共接口app.getComponentCount function() { return components.length; }; app.registerComponent function(component) { if (typeof component object) { components.push(component); } }; // 返回实例return app; }();在这个重写的 application 单例对象的例子中首先定义了私有变量和私有函数跟之前例子中一样。主要区别在于这里创建了一个名为 app 的变量其中保存了 BaseComponent 组件的实例。这是最终要变成 application 的那个对象的局部版本。在给这个局部变量 app 添加了能够访问私有变量的公共方法之后匿名函数返回了这个对象。然后这个对象被赋值给 application。 17. 小结 函数表达式与函数声明是不一样的。函数声明要求写出函数名称而函数表达式并不需要。没有名称的函数表达式也被称为匿名函数。ES6 新增了类似于函数表达式的箭头函数语法但两者也有一些重要区别。JavaScript 中函数定义与调用时的参数极其灵活。arguments 对象以及 ES6 新增的扩展操符 可以实现函数定义和调用的完全动态化。函数内部也暴露了很多对象和引用涵盖了函数被谁调用、使用什么调用以及调用时传入了什么参数等信息。JavaScript 引擎可以优化符合尾调用条件的函数以节省栈空间。闭包的作用域链中包含自己的一个变量对象然后是包含函数的变量对象直到全局上下文的变量对象。通常函数作用域及其中的所有变量在函数执行完毕后都会被销毁。闭包在被函数返回之后其作用域会一直保存在内存中直到闭包被销毁。函数可以在创建之后立即调用执行其中代码之后却不留下对函数的引用。立即调用的函数表达式如果不在包含作用域中将返回值赋给一个变量则其包含的所有变量都会被销毁。虽然 JavaScript 没有私有对象属性的概念但可以使用闭包实现公共方法访问位于包含作用域 中定义的变量。可以访问私有变量的公共方法叫作特权方法。特权方法可以使用构造函数或原型模式通过自定义类型中实现也可以使用模块模式或模块增 强模式在单例对象上实现。
http://www.zqtcl.cn/news/315119/

相关文章:

  • 莱芜做网站公司网站建设表单教案
  • 建设酒类产品网站的好处遵义网站制作费用
  • 高端网站设计价格wordpress登录下载附件
  • 国内有名的网站设计公司wordpress缓存插件比拼
  • 网站的建设和推广直播营销策划方案范文
  • 做购物平台网站 民治百度导航地图下载
  • 东莞市主营网站建设服务机构青岛建站公司电话
  • 做网站技术wordpress漂亮手机网站模板下载
  • 网站怎么更新网页内容网络推广怎么找客户
  • 如何编写网站建设销售的心得适合装饰公司的名字
  • 有什么免费建网站网站pr查询
  • flash+xml网站模板简述网站制作的一般流程
  • 成都私人做网站建设怎么切页面做网站
  • 聊城做网站的公司论坛外链代发
  • 廊坊企业自助建站网站框架设计好后怎么做
  • 手机网站建设效果wordpress 目录改变
  • 做商城网站的项目背景图片c2750服务器做网站行吗
  • 北京市专业网站建设wordpress视频站
  • 知名网站制作公南充建设机械网站
  • 网站建设实践鉴定微商小程序制作
  • 盗用别人网站图做网站快速排名优化推广手机
  • 安徽网站建设服务平台wordpress自定义统计
  • 微网站开发方案模板建站宝盒哪个牌子好
  • 低价做网站网站制作天津
  • 成都网站推广外包门户网站素材
  • wordpress 拿站网站制作北京海淀
  • 惠州网站建设行业wordpress文章阅读权限
  • 做地方网站需要什么部门批准网页版微信二维码
  • o2o网站运维建设方案宿州市网站建设
  • 上海网站排名优化公司马蜂窝是什么做的网站