第1章 值、类型和运算符
1.1 值
在JavaScript中包含6种基本的值类型:数字(number)、字符串(string)、布尔值(boolean)、对象(object)、函数(function)、和未定义类型(undefined)
1.2 数字
数字(number)类型的值即数字值。在JavaScript中写成如下形式:
13
注意:在处理分数的时候,将其视为近似值,而非精确值
1.2.1 算术
JavaScript中的算术运算如下所示:
100 + 4 * 11
1.2.2 特殊数字
正无穷大: Infinity
负无穷大:-Infinity
非数值: NaN
1.3 字符串
我们使用字符串来表示文本信息。使用引号将内容括起来。
"Patch my boat with chewing gum"
'Monkeys wave goodbye'
1.4 一元运算符
console.log(typeof 4.5); // numberconsole.log(typeof "x"); // string复制代码
1.5 布尔值
该类型只有两种取值:true和false
1.5.1 比较
console.log(3 > 2); // trueconsole.log(3 < 2); // falseconsole.log("Aardark" < "Zoroaster"); // trueconsole.log( NaN == NaN); // false复制代码
1.5.2 逻辑运算符
JavaScript支持三种逻辑运算符:与(and)、或(or)、非(not)
console.log(true && false); // falseconsole.log(true && true); // trueconsole.log(false || true); // trueconsole.log(false || false); // false// 三元运算符console.log(true ? 1 : 2); // 1console.log(false ? 1 : 2); // 2复制代码
1.6 未定义值
null和undefined用于表示无意义的值。它们各自表示其自身含义,除此之外不包含任何信息。
1.7 自动类型转换
console.log(8 * null); // 0console.log("5" - 1); // 4console.log("5" + 1); // 51console.log("five" * 2); // NaNconsole.log(false == 0); // trueconsole.log("" === false); // false复制代码
逻辑运算符的短路特性
console.log(null || "user"); // userconsole.log("Karel" || "user"); // Karel复制代码
第2章 程序结构
2.1 表达式和语句
最简单的一条语句由一个表达式和其后的分号组成。比如这就是一个程序:
1;!false;复制代码
2.2 变量
var caught = 5 * 5;var ten = 10;console.log(ten * ten); // 100var mood = "light";console.log(mood); // lightmood = "dark";console.log(mood); // darkvar luigisDebt = 140;luigisDebt = luigisDebt - 35;console.log(luigisDebt); // 105var one = 1,two = 2;console.log(one + two); // 3复制代码
2.3 关键字和保留字
某些具有特殊含义的单词称之为关键字,比如var,关键字不能作为变量名。另外还有一些单词预留给JavaScript使用。
2.4 环境
我们将给定时间内的变量和变量值的集合称为环境。
2.5 函数
在默认环境中包含了很多函数类型的值。函数是指包装在变量中的一段程序,我们可以使用这些值来执行包装好的程序。
alert("Good morning!");复制代码
2.6 console.log函数
var x = 30;console.log("the value of x is ",x); // the value of x is 30复制代码
2.7 返回值
我们将函数生成值的操作称之为返回值。
console.log(Math.max(2,4); // 4console.log(Math.min(2,4) + 100); // 102复制代码
2.8 prompt和confirm函数
在现代web编程当中很少使用这两个函数,主要原因是你无法对弹出窗口的风格进行控制。但对于小程序或测试程序来说,这两个函数还是非常有用的。
confirm("Shall we,then?");prompt("Tell me everything you know.","...");复制代码
2.9 控制流
若你的程序中包含了不止一条语句,那么这些语句一定是按照从上到下的顺序执行的。
var theNumber = Number(prompt("Pick a number","");alert("Your number is the square root of " + theNumber * theNumber);复制代码
2.10 条件执行
var theNumber = Number(prompt("Pick a number","");if(!isNaN(theNumber)){ alert("Your number is the square root of " + theNumber * theNumber);}else{ alert("Hey,Why didn't you give me a number?");}var num = Number(prompt("Pick a number","");if(num < 10){ alert("small");}else if(num < 100){ alert("medium");}else{ alert("large");}复制代码
2.11 while和do循环
var number = 0;while(number <= 12){ console.log(number); number = number + 2;}// 编写一个程序用于显示2的10次方var result = 1;var counter = 0;while(counter < 10){ result = result * 2; counter++;}console.log(result);do{ var name = prompt("who are you?");}while(!name);console.log(name);复制代码
2.12 代码缩进
2.13 for循环
for(var number = 0; number <= 12;number = number + 2){ console.log(number);}var result = 1;for(var counter = 0;counter < 10;counter++){ result = result * 2;}console.log(result);复制代码
2.14 跳出循环
for(var current = 20; ; current++){ if(current % 7 == 0){ break; }}console.log(current); // 21复制代码
2.15 更新变量的简便方法
counter += 1;result *= 2;counter++;counter--;复制代码
2.16 switch条件分支
switch(prompt("What is the weather like?")){ case "rainly": console.log("Remember to bring an umbrella"); break; case "sunny": console.log("Dress lightly"); break; case "cloudly": console.log("Go outside"); break; default: console.log("Unknown weather type!"); break;}复制代码
2.17 大写
2.18 注释
var accountBalance = calculateBalance(account);// It's a green hollow where a river singsaccountBalance.adjust();// Madly catching white tatters in the grassvar report = new Report();// Where the sun on the proud mountain rings:addToReport(accountBalance,report);// It's a little valley,foaming like light in a glass/*I first found ths number scrawled on the back of one ofmy notebooks a few years ago.Since then,it has oftendropped by,showing up in phone numbers and the serial numbers of products that I've bought.It obviously likesme,so I've decided to keep it.*/var myNumber = 112213;复制代码
2.19 本章小结
2.20 习题
// 1var str = "";for(var i = 0; i < 7;i++){ str = str + "#"; console.log(str);}// 2for(var i = 1; i <= 100;i++){ if(i % 3 == 0){ console.log("Fizz"); }else if(i % 5 == 0){ console.log("Buzz") }else{ console.log(i); }}for(var i = 1; i <= 100;i++){ if(i % 3 == 0 && i % 5 == 0){ console.log("FizzBuzz"); }else if(i % 3 == 0){ console.log("Fizz") }else if(i % 5 == 0){ console.log("Buzz") }else{ console.log(i); }}// 3var str = "";var size = 8;for(var i = 0; i < size; i++){ for(var j = 0; j < size;j++){ if((i + j)% 2 == 0){ str = str + "#"; }else{ str = str + " "; } } str += "\n"; }console.log(str);复制代码
第3章 函数
3.1 定义函数
一个函数定义就是普通的变量定义,只不过变量类型恰好是函数。
创建函数的表达式以关键字function开头。
某些函数可以产生值,而一些函数不会产生值。函数中的return语句决定了函数的返回值。当函数执行到return语句时,会立即跳出当前函数,并将返回值赋给调用者。如果return关键字后没有任何表达式,则该函数返回undefined。
// 计算给定数字的平方var square = functon(x) { return x*x;}console.log(square(12)); // 144var makeNoise = function() { console.log("Pling!");}makeNoise(); // Pling!var power = function(base,exponent) { var result = 1; for(var count = 0;count < exponent;count++) { result *= base; return result; }}console.log(power(2,10)); // 1024复制代码
3.2 参数和作用域
函数的参数如同一个普通变量,但其初始值是由函数的调用者提供的,而不是函数自身的内部代码。
函数有一个重要属性,就是其内部创建的变量以及参数,都属于函数的局部变量。这意味着示例中power函数的result变量会在每次函数调用重新创建,这种隔离机制确保了不同函数之间不会相互干扰。
var x = "outside";var f1 = function() { var x = "inside f1";}f1();console.log(x); // outsidevar f2 = function() { x = "inside f2";}f2();console.log(x); // inside f2复制代码
3.3 嵌套作用域
JavaScript不仅能区分全局变量和局部变量,还可以在函数中创建其他函数,并产生不同程度的局部作用域。
var landscape = function(){ var result = ""; var flat = function(size){ for(var count = 0;count < size;count++){ result +="_"; } } var mountain = function(size) { result +="/"; for(var count = 0;count < size;count++){ result += "'"; } result += "\\"; } flat(3); mountain(4); flat(6); mountain(1); flat(1); return result; }; console.log(landscape());复制代码
3.4 函数值
函数变量通常只是充当一段特定程序的名字。这种变量只需要定义一次,而且永远不会改变。
你不仅可以直接调用函数,还可以像使用其他类型的值一样使用函数类型的值、将其用在任何表达式中、将其存放在新的变量中或者将其作为参数传递给其他函数等。同理,一个用于存放函数的变量也只不过是一个普通变量,可以被赋予新的值。
var launchMissiles = function(value) { missileSystem.launch("now");};if(safeMode){ launchMissiles = function(value){ // do something }}复制代码
3.5 符号声明
相较于使用“var square = function...”表达式来声明函数,还可以使用另一种更为简洁的方法声明函数。
function square(x) { return x*x;}复制代码
从概念上看,函数声明会被移动到其作用域的顶端,所有作用域内的函数调用都不会有任何问题。
console.log("The future says:",future());function future(){ return "We STILL have no flying cars."} console.log("The future says:",future1()); // Uncaught TypeError: future1 is not a function var future1 = function () { return "1111"; }复制代码
3.6 调用栈
function greet(who){ console.log("Hello" + who);}greet("Harry");console.log("Bye");复制代码
由于函数需要在执行结束后跳转回调用该函数的代码位置,因此计算机必须记住函数调用的上下文。
我们将计算机存储这个上下文的区域称之为调用栈。每当函数调用时,当前的上下文信息就会被存储在栈顶。当函数返回时,系统会删除存储在栈顶的上下文信息,并使用该信息继续执行程序。
栈需要保存在计算机内存中。若栈存储的空间过大,计算机就会提示类似于“栈空间溢出”或“递归过多”的信息。
function chicken(){ return egg();}function egg(){ return chicken();}console.log(chicken() + "came first.");复制代码
3.7 可选参数
JavaScript对传入函数的参数数量几乎不做任何限制。如果你传递了过多参数,多余的参数就会被忽略掉,而如果你传递的参数过少,遗漏的参数将会被赋值成undefined。
该特性的缺点是你可能恰好向函数传递了错误数量的参数,但没有人会告诉你这个错误。
该特性的优点是我们可以利用这种行为来让函数接收可选参数。
function power(base,exponent){ if(exponent == undefined) { exponent = 2; } var result = 1; for(var count = 0;count < exponent;count++){ result *= base; } return result;}console.log(power(4)); // 16console.log(power(4,3)); // 64复制代码
3.8 闭包
函数可以作为值使用,而且其局部变量会在每次函数调用时重新创建。
function wrapValue(n) { var localVariable = n; return function() { return localVariable; }}var wrap1 = wrapValue(1);var wrap2 = wrapValue(2);console.log(wrap1()); // 1console.log(wrap2()); // 2复制代码
其实同一变量的多个实例可以同时存在,这也就很好地印证了局部变量会在每次函数调用时重新创建,不同的函数调用是不会对其他函数内的局部变量产生影响的。
我们把这种引用特定的局部变量实例的功能称为闭包。一个包装了一些局部变量的函数是一个闭包。
function multiplier(factor) { return function(number) { return number * factor; }}var twice = multiplier(2);console.log(twice(5)); // 10复制代码
3.9 递归
函数完全可以自己调用自己,只要避免栈溢出的问题即可。我们把函数调用自身的行为称为递归。
function power(base,exponent){ if(exponent == 0){ return 1; }else{ return base * power(base,exponent -1); }}console.log(power(2,3)); // 8复制代码
在标准的JavaScript实现当中,递归写法的函数执行效率比循环写法的函数慢了大约10倍。执行简单的循环操作比多次函数调用效率要高很多。
基本原则:除非程序执行速度确实太慢,否则先不要关注效率问题。一旦出现速度太慢的情况,找出占用时间最多的部分,然后将其替换成效率更高的代码。
但是,我们并不能以偏概全的说递归就只是比循环的效率低。对于某些问题来说,递归相较于循环更能解决问题。这类问题通常需要执行和处理多个分支,而每个分支又会引出更多的执行分支。
// 从数字1开始,每次可以将数字加5或乘以3,循环执行。这样可以产生无穷多个新数字。那么如何编写函数来找出一个加法乘法序列,以产生指定的数字呢?例如,数字13可以通过1次乘法和2次加法生成,而数字15永远无法得到。使用递归编码的解决方案如下:function findSolution(target){ function find(start,history){ if(start == target){ return history; }else if(start > target){ return null; }else{ return find(start + 5,"(" + history + "+5 )") || find(start * 3,"(" + history + "*3 )") } } return find(1,"1");}console.log(findSolution(24));复制代码
3.10 添加新函数
这里有两种常用的方法将函数引入到程序中。
第一种方法是找出你的程序中多次出现的相似代码。我们可以把重复功能的代码提取出来放到一个函数中去,并起一个合适的名字。
第二种方法是当你写一些新功能代码,并觉得这些代码应该成为一个函数时,我们将这部分代码写到一个函数中,并取一个函数名。
// 编写一个打印两个数字的程序,第一个数字是农场中牛的数量,第二个数字是农场中鸡的数量,并在数字后面跟上Cows 和Chickens用以说明,并且在两个数字前填充0,以使得每个数字总是由三位数字组成。function printFarmInventory(cows,chickens) { var cowString = String(cows); while(cowString.length < 3){ cowString = "0" + cowString; } console.log(cowString + " cows"); var chickenString = String(chickens); while(chickenString.length < 3){ chickenString = "0" + chickenString; } console.log(chickenString + " Chickens");}printFarmInventory(7,11);// 扩展软件来输出猪的数量function printZeroPaddedWithLabel(number,label){ var numberString = String(number); while(numberString.length < 3){ numberString = "0" + numberString; } console.log(numberString + " " + label);}function printFarmInventory(cows,chickens,pigs){ printZeroPaddedWithLabel(cows,"Cows"); printZeroPaddedWithLabel(chickens,"Chickens"); printZeroPaddedWithLabel(pigs,"Pigs");}printFarmInventory(7,11,3);// 继续优化function zeroPad(number,width){ var string = String(number); while(string.length < width){ string = "0" + string; } return string;}function printFarmInventory(cows,chickens,pigs){ console.log(zeroPad(cows,3) + "Cows"); console.log(zeroPad(chickens,3) + "Chickens"); console.log(zeroPad(pigs,3) + "pigs");}printFarmInventory(7,16,3); 复制代码
比较好的方法是,尽量不要在函数中添加过多功能,除非你真的需要它。我们总是希望能够编写一个通用的框架来解决遇到的各类问题。但是别急,这么做其实并不能解决任何问题,而且不会有人真的去使用这样的代码。
3.11 函数及其副作用
我们可以将函数分成两类:一类调用后产生副作用,而另一类则产生返回值(当然我们也可以定义同时产生副作用和返回值的函数)。
相比于直接产生副作用的函数,产生返回值的函数则更容易集成到新的环境当中使用。
纯函数是一种只会产生值而且不会影响它自己范围外任何事情的函数。
3.12 本章小结
3.13 习题
// 1function min(a,b){ if(a> b){ return b; }else{ return a; }}console.log(min(1,2));// 2function isEven(n){ if(n == 0){ return true; }else if(n == 1){ return false; }else if (n < 0){ return isEven(-n); }else{ return isEven(n-2); }}console.log(isEven(50)); // trueconsole.log(isEven(75)); // falseconsole.log(isEven(-1)); // false// 3function countBs(str){ var count = 0; for(var i = 0;i < str.length;i++){ if(str[i] == 'B'){ count++; } } return count;}console.log(countBs("12B45B12B"));function countChar(str,target){ var count = 0; for(var i = 0; i < str.length;i++){ if(str[i] == target){ count++; } } return count;}console.log(countChar("12A345AaA","A"));// 官方正解function countChar(string, ch) { let counted = 0; for (let i = 0; i < string.length; i++) { if (string[i] == ch) { counted += 1; } } return counted;}function countBs(string) { return countChar(string, "B");}console.log(countBs("BBC"));console.log(countChar("kakkerlak", "k"));复制代码
第4章 数据结构:对象和数组
数字、布尔值和字符串构成了基本的数据结构。少了其中任何一样,你可能都很难构造出完整的结构。我们可以使用对象来把值和其他对象组织起来,通过这种手段来构造更为复杂的结构。
4.1 松鼠人
4.2 数据集
JavaScript提供了一种数据类型,专门用于存储一系列的值。我们将这种数据类型称为数组(array),将一连串的值写在方括号当中,值之间使用逗号(,)分隔。
var listOfNumbers = [2,3,5,7,11];console.log(listOfNumbers[1]); // 3console.log(listOfNumbers[1 -1]); // 2复制代码
4.3 属性
在JavaScript中,几乎所有的值都有属性。但null和undefined没有。
在JavaScript中有两种最为常用的访问属性的方法:使用点(.)和方括号[]。 如果使用点,则点之后的部分必须是一个合法变量名,即直接写属性名称。如果使用方括号,则JavaScript会将方括号中表达式的返回值作为属性名称。
4.4 方法
除了length属性以外,字符串和数组对象还包含了许多其他属性,这些属性是函数值。
var doh = "Doh";console.log(typeof doh.toUpperCase); // functionconsole.log(doh.toUpperCase()); // DOH复制代码
我们通常将包含函数的属性称为某个值的方法(method)。比如说,“toUpperCase是字符串的一个方法”。
var mack = [];mack.push("Mack");mack.push("the","knife");console.log(mack);console.log(mack.join(" "));console.log(mack.pop());console.log(mack);复制代码
我们可以使用push方法向数组的末尾添加值,pop方法则作用相反:删除数组末尾的值,并返回给调用者。我们可以使用join方法将字符串数组拼接成单个字符串,join方法的参数是连接数组元素之间的文本内容。
4.5 对象
对象类型的值可以存储任意类型的属性,我们可以随意增删这些属性。一种创建对象的方法是使用大括号。
var day1 = { squirrel: false, events: ["work","touched tree","pizza","running","television"]};console.log(day1.squirrel); // falseconsole.log(day1.wolf); // undefinedday1.wolf = false;console.log(day1.wolf); // false复制代码
在大括号中,我们可以添加一系列的属性,并用逗号分隔。每一个属性均以名称开头,紧跟一个冒号,然后是对应属性的表达式。如果属性名不是有效的变量名或者数字,则需要使用引号将其括起来。
var description = { work: "Went to work", "touched tree": "Touched a tree"}复制代码
读取一个不存在的属性就会产生undefined值。
delete是个一元运算符,其操作数是访问属性的表达式,可以从对象中移除指定属性。
var anObject = { left: 1, right: 2}console.log(anObject.left); // 1delete anObject.left;console.log(anObject.left); // undefinedconsole.log("left" in anObject); // falseconsole.log("right" in anObject); // true复制代码
二元运算符in的第一个操作数是一个表示属性名的字符串,第二个操作数是一个对象,它会返回一个布尔值,表示该对象是否包含该属性。将属性设置为undefined与使用delete删除属性的区别在于:对于第一种情况,对象仍然包含left属性,只不过该属性没有引用任何值;对于第二种情况,对象中已不存在left属性,因此in运算符会返回false。
数组只不过是一种用于存储数据序列的特殊对象,因此typeof[1,2]的执行结果是“Object”。
// 我们可以用一个数组对象来表示雅克的日志var journal = [{ events: ["work","touched tree","pizza","running","television"], squirrel: false},{ events: ["work","ice cream","cauliflower","lasagna","touched tree","brushed teeth"], squirrel: false}/* and so on */];复制代码
4.6 可变性
数字、字符串和布尔值都是不可变值,我们无法修改这些类型值的内容。
但对于对象来说,我们可以通过修改其属性来改变对象的内容。
var object1 = {value: 10};var object2 = object1;var object3 = {value: 10};console.log(object1 == object2); // trueconsole.log(object1 == object3); // falseobject1.value = 15;console.log(object2.value); // 15console.log(object3.value); // 10复制代码
在JavaScript中,使用==运算符来比较两个对象时,只有两个对象引用了同一个值,结果才会返回true。比较两个不同的对象将会返回false,哪怕对象内容相同。JavaScript中没有内置深度比较运算符(比较对象内容),但你可以自己编写一个。
4.7 松鼠人的记录
var journal = [];function addEntry(events,didITurnIntoASquirrel){ journal.push({ events: events, squirrel: didITurnIntoASquirrel })}addEntry(["work","touched tree","pizza","running","television"],false);addEntry(["work","ice cream","cauliflower","lasagna","touched tree","brushed teeth"],false);addEntry(["weekend","cycling","break","peanuts","beer"],true);复制代码
4.8 计算关联性
// 计算数组的系数Φfunction phi(table) { return (table[3] * table[0] - table[2] * table[1])/Math.sqrt((table[2] + table[3]) * (table[0] + table[1]) * (table[1] + table[3]) * (table[0] + table[2]));}console.log(phi([76,9,4,1]));// 循环遍历整个记录,并计算出与变身成松鼠相关事件发生的次数。var JOURNAL = [ { "events":["carrot","exercise","weekend"],"squirrel":false}, { "events":["bread","pudding","brushed teeth","weekend","touched tree"],"squirrel":false}, { "events":["carrot","nachos","brushed teeth","cycling","weekend"],"squirrel":false}, { "events":["brussel sprouts","ice cream","brushed teeth","computer","weekend"],"squirrel":false}, { "events":["potatoes","candy","brushed teeth","exercise","weekend","dentist"],"squirrel":false}, { "events":["brussel sprouts","pudding","brushed teeth","running","weekend"],"squirrel":false}, { "events":["pizza","brushed teeth","computer","work","touched tree"],"squirrel":false}, { "events":["bread","beer","brushed teeth","cycling","work"],"squirrel":false}, { "events":["cauliflower","brushed teeth","work"],"squirrel":false}, { "events":["pizza","brushed teeth","cycling","work"],"squirrel":false}, { "events":["lasagna","nachos","brushed teeth","work"],"squirrel":false}, { "events":["brushed teeth","weekend","touched tree"],"squirrel":false}, { "events":["lettuce","brushed teeth","television","weekend"],"squirrel":false}, { "events":["spaghetti","brushed teeth","work"],"squirrel":false}, { "events":["brushed teeth","computer","work"],"squirrel":false}, { "events":["lettuce","nachos","brushed teeth","work"],"squirrel":false}, { "events":["carrot","brushed teeth","running","work"],"squirrel":false}, { "events":["brushed teeth","work"],"squirrel":false}, { "events":["cauliflower","reading","weekend"],"squirrel":false}, { "events":["bread","brushed teeth","weekend"],"squirrel":false}, { "events":["lasagna","brushed teeth","exercise","work"],"squirrel":false}, { "events":["spaghetti","brushed teeth","reading","work"],"squirrel":false}, { "events":["carrot","ice cream","brushed teeth","television","work"],"squirrel":false}, { "events":["spaghetti","nachos","work"],"squirrel":false}, { "events":["cauliflower","ice cream","brushed teeth","cycling","work"],"squirrel":false}, { "events":["spaghetti","peanuts","computer","weekend"],"squirrel":true}, { "events":["potatoes","ice cream","brushed teeth","computer","weekend"],"squirrel":false}, { "events":["potatoes","ice cream","brushed teeth","work"],"squirrel":false}, { "events":["peanuts","brushed teeth","running","work"],"squirrel":false}, { "events":["potatoes","exercise","work"],"squirrel":false}, { "events":["pizza","ice cream","computer","work"],"squirrel":false}, { "events":["lasagna","ice cream","work"],"squirrel":false}, { "events":["cauliflower","candy","reading","weekend"],"squirrel":false}, { "events":["lasagna","nachos","brushed teeth","running","weekend"],"squirrel":false}, { "events":["potatoes","brushed teeth","work"],"squirrel":false}, { "events":["carrot","work"],"squirrel":false}, { "events":["pizza","beer","work","dentist"],"squirrel":false}, { "events":["lasagna","pudding","cycling","work"],"squirrel":false}, { "events":["spaghetti","brushed teeth","reading","work"],"squirrel":false}, { "events":["spaghetti","pudding","television","weekend"],"squirrel":false}, { "events":["bread","brushed teeth","exercise","weekend"],"squirrel":false}, { "events":["lasagna","peanuts","work"],"squirrel":true}, { "events":["pizza","work"],"squirrel":false}, { "events":["potatoes","exercise","work"],"squirrel":false}, { "events":["brushed teeth","exercise","work"],"squirrel":false}, { "events":["spaghetti","brushed teeth","television","work"],"squirrel":false}, { "events":["pizza","cycling","weekend"],"squirrel":false}, { "events":["carrot","brushed teeth","weekend"],"squirrel":false}, { "events":["carrot","beer","brushed teeth","work"],"squirrel":false}, { "events":["pizza","peanuts","candy","work"],"squirrel":true}, { "events":["carrot","peanuts","brushed teeth","reading","work"],"squirrel":false}, { "events":["potatoes","peanuts","brushed teeth","work"],"squirrel":false}, { "events":["carrot","nachos","brushed teeth","exercise","work"],"squirrel":false}, { "events":["pizza","peanuts","brushed teeth","television","weekend"],"squirrel":false}, { "events":["lasagna","brushed teeth","cycling","weekend"],"squirrel":false}, { "events":["cauliflower","peanuts","brushed teeth","computer","work","touched tree"],"squirrel":false}, { "events":["lettuce","brushed teeth","television","work"],"squirrel":false}, { "events":["potatoes","brushed teeth","computer","work"],"squirrel":false}, { "events":["bread","candy","work"],"squirrel":false}, { "events":["potatoes","nachos","work"],"squirrel":false}, { "events":["carrot","pudding","brushed teeth","weekend"],"squirrel":false}, { "events":["carrot","brushed teeth","exercise","weekend","touched tree"],"squirrel":false}, { "events":["brussel sprouts","running","work"],"squirrel":false}, { "events":["brushed teeth","work"],"squirrel":false}, { "events":["lettuce","brushed teeth","running","work"],"squirrel":false}, { "events":["candy","brushed teeth","work"],"squirrel":false}, { "events":["brussel sprouts","brushed teeth","computer","work"],"squirrel":false}, { "events":["bread","brushed teeth","weekend"],"squirrel":false}, { "events":["cauliflower","brushed teeth","weekend"],"squirrel":false}, { "events":["spaghetti","candy","television","work","touched tree"],"squirrel":false}, { "events":["carrot","pudding","brushed teeth","work"],"squirrel":false}, { "events":["lettuce","brushed teeth","work"],"squirrel":false}, { "events":["carrot","ice cream","brushed teeth","cycling","work"],"squirrel":false}, { "events":["pizza","brushed teeth","work"],"squirrel":false}, { "events":["spaghetti","peanuts","exercise","weekend"],"squirrel":true}, { "events":["bread","beer","computer","weekend","touched tree"],"squirrel":false}, { "events":["brushed teeth","running","work"],"squirrel":false}, { "events":["lettuce","peanuts","brushed teeth","work","touched tree"],"squirrel":false}, { "events":["lasagna","brushed teeth","television","work"],"squirrel":false}, { "events":["cauliflower","brushed teeth","running","work"],"squirrel":false}, { "events":["carrot","brushed teeth","running","work"],"squirrel":false}, { "events":["carrot","reading","weekend"],"squirrel":false}, { "events":["carrot","peanuts","reading","weekend"],"squirrel":true}, { "events":["potatoes","brushed teeth","running","work"],"squirrel":false}, { "events":["lasagna","ice cream","work","touched tree"],"squirrel":false}, { "events":["cauliflower","peanuts","brushed teeth","cycling","work"],"squirrel":false}, { "events":["pizza","brushed teeth","running","work"],"squirrel":false}, { "events":["lettuce","brushed teeth","work"],"squirrel":false}, { "events":["bread","brushed teeth","television","weekend"],"squirrel":false}, { "events":["cauliflower","peanuts","brushed teeth","weekend"],"squirrel":false}];function hasEvent(event,entry) { return entry.events.indexOf(event) != -1;}function tableFor(event,journal) { var table = [0,0,0,0]; for(var i = 0; i < journal.length; i++){ var entry = journal[i]; var index = 0; if(hasEvent(event,entry)){ index += 1; } if(entry.squirrel){ index += 2; } table[index] += 1; } return table;}console.log(tableFor("pizza",JOURNAL));复制代码
4.9 对象映射
映射表(map)可以通过一个值(在本例中是事件名)来获取对应的另一个值(在本例中是Φ系数)。
var map = {};function storePhi(event,phi) { map[event] = phi;}storePhi("pizza",0.069);storePhi("touched tree",-0.081);console.log("pizza" in map); // trueconsole.log(map["touched tree"]); // -0.081复制代码
JavaScript提供了另一种遍历对象属性的循环语句。它与一般的for循环看起来很像,只是我们使用的关键字不是for而是in。
for(var event in map) { console.log("The correlation for '" + event + "' is " + map[event]);}// The correlation for 'pizza' is 0.069// The correlation for 'touched tree' is -0.081复制代码
4.10 分析结果
为了找出数据集中存在的所有事件类型,我们只需依次处理每条记录,然后遍历记录中的所有事件即可。
function gatherCorrelations(journal) { var phis = {}; for(var entry = 0;entry < journal.length;entry++){ var events = journal[entry].events; for(var i = 0; i < events.length;i++){ var event = events[i]; if(!(event in phis)){ phis[event] = phi(tableFor(event,journal)); } } } return phis;}var correlations = gatherCorrelations(JOURNAL);console.log(correlations.pizza);for(var event in correlations) { console.log(event + ":" + correlations[event]);}for(var event in correlations) { var correlation = correlations[event]; if(correlation > 0.1 || correlation < -0.1){ console.log(event + ":" + correlation); }}for(var i = 0;i < JOURNAL.length;i++){ var entry = JOURNAL[i]; if(hasEvent("peanuts",entry) && !hasEvent("brushed teeth",entry)){ entry.events.push("peanut teeth"); }}console.log(phi(tableFor("peanut teeth",JOURNAL))); // 1复制代码
4.11 详解数组
一些实用的数组方法
push和pop,分别用于在数组末尾添加或删除元素。
unshift和shift,分别用于在数组的开头添加或删除元素。
var todoList = [];function rememberTo(task) { todoList.push(task);}function whatIsNext() { return todoList.shift();}function urgentlyRememberTo(task) { todoList.unshift(task);}复制代码
indexOf, 从数组第一个元素向后搜索。
lastIndexOf,从数组最后一个元素向前搜索。
indexOf和lastIndexOf方法都有一个可选参数,可以用来指定搜索的起始位置。
console.log([1,2,3,2,1].indexOf(2)); // 1console.log([1,2,3,2,1].lastIndexOf(2)); // 3复制代码
slice,该方法接受一个起始索引和一个结束索引,然后返回数组中两个索引范围内的元素。起始索引元素包含在返回结果中,但结束索引元素不会包含在返回结果中。如果没有指定结束索引,slice会返回从起始位置之后的所有元素。对于字符串来说,它也有一个具有相同功能的slice方法供开发人员使用。
console.log([0,1,2,3,4].slice(2,4)); // [2,3]console.log([0,1,2,3,4].slice(2)); // [2,3,4]复制代码
concat 方法用于拼接两个数组,其作用类似于字符串的+运算符。
function remove(array,index) { return array.slice(0,index).concat(array.slice(index + 1 ));}console.log(remove(["a","b","c","d","e"],2)); // ["a","b","d","e"]复制代码
4.12 字符串及其属性
我们可以调用字符串的length或toUpperCase这样的属性,但不能向字符串中添加任何新的属性。
var myString = "Fido";myString.myProperty = "value";console.log(myString.myProperty); // undefined复制代码
字符串、数字和布尔类型的值并不是对象,因此当你向这些值中添加属性时JavaScript并不会报错,但实际上你并没有将这些属性添加进去。这些值都是不可变的,而且无法向其中添加任何属性。
但这些类型的值包含一些内置属性。每个字符串中包含了若干方法供我们使用,最有用的方法可能就是slice和indexOf了,它们的功能与数组中的同名方法类似。
console.log("coconuts".slice(4,7)); // nutconsole.log("coconuts".indexOf("u")); // 5复制代码
唯一的区别在于,字符串的indexOf方法可以使用多个字符作为搜索条件,而数组中的indexOf方法则只能搜索单个元素。
console.log("one two three".indexOf("ee")); // 11复制代码
trim方法用于删除字符串中开头和结尾的空白符号(空格、换行和制表符等符号)。
console.log(" okay \n ".trim()); // okay复制代码
// 获取字符串中某个特定的字符var string = "abc";console.log(string.length); // 3console.log(string.charAt(0)); // aconsole.log(string[1]); // b复制代码
4.13 arguments对象
每当函数被调用时,就会在函数体的运行环境当中添加一个特殊的变量arguments。该变量指向一个包含了所有入参的对象。在JavaScript中,我们可以传递多于(或少于)函数参数列表定义个数的参数。
function noArguments(){};noArguments(1,2,3); // This is okayfunction threeArgumnents(a,b,c){};threeArguments(); // And so is this复制代码
arguments对象有一个length属性,表示实际传递给函数的参数个数。每个参数对应一个属性,被命名为0,1,2,以此类推。
function argumentCounter() { console.log("You gave me",arguments.length,"arguments.");}argumentCounter("Straw man","Tautology","Ad hominem"); // You gave me 3 arguments.复制代码
function addEntry(squirrel){ var entry = { events: [], squirrel: squirrel }; for(var i = 1; i < arguments.length; i++){ entry.events.push(arguments[i]); } journal.push(entry);}addEntry(true,"work","touched tree","pizza","running","television");复制代码
4.14 Math对象
Math对象简单地把一组相关的功能打包成一个对象供用户使用。全局只有一个Math对象,其对象本身没有什么实际用途。Math对象其实提供了一个“命名空间”,封装了所有的数学运算函数和值,确保这些元素不会变成全局变量。
过多的全局变量会对命名空间造成“污染”。全局变量越多,就越有可能一不小心把某些变量的值覆盖掉。
function randomPointOnCircle(radius) { var angle = Math.random() * 2 * Math.PI; return { x: radius * Math.cos(angle), y: radius * Math.sin(angle) };} console.log(randomPointOnCircle(2));复制代码
Math.random,每次调用该函数时,会返回一个伪随机数,范围在0(包括) ~ 1(不包括)之间。
console.log(Math.random());console.log(Math.random());console.log(Math.random());复制代码
Math.floor,向下取整
Math.ceil,向上取整
Math.round,四舍五入
console.log(Math.floor(Math.random() * 10)); // 等概率地取到0~9中的任何一个数字。复制代码
4.15 全局对象
JavaScript全局作用域中有许多全局变量,都可以通过全局对象进行访问。每一个全局变量作为一个属性存储在全局对象当中。在浏览器中,全局对象存储在window变量当中。
var myVar = 10;console.log("myVar" in window); // trueconsole.log(window.myVar); // 10复制代码
4.16 本章小结
对象和数组(一种特殊对象)可以将几个值组合起来形成一个新的值。
在JavaScript中,除了null和undefined以外,绝大多数的值都含有属性。
在数组中有一些具名属性,比如length和一些方法。
对象可以用作映射表,将名称与值关联起来。我们可以使用in运算符确定对象中是否包含特定名称的属性。我们同样可以在for循环中(for(var name in object))使用关键字in来遍历对象中包含的属性。
4.17 习题
// 1 编写一个range函数,接受两个参数:start和end,然后返回包含start到end(包括end)之间的所有数字。function range(start,end){var arr = [];if(start > end){ for(var i = start; i >= end;i--){ arr.push(i); }}else{ for(var i = start; i <= end;i++){ arr.push(i); }} return arr;}console.log(range(1,10));console.log(range(5,2,-1));// 2 编写一个sum函数,接受一个数字数组,并返回所有数字之和function sum(arr){ var sum = 0; for(var i = 0; i < arr.length;i++){ sum += arr[i]; } return sum;}console.log(sum(range(1, 10)));// 3 附加题修改range函数,接受第3个可选参数,指定构建数组时的步数(step)。 function range(start,end,step){ var arr = []; if(step == undefined){ step = 1; } if(start > end){ for(var i = start; i >= end;i= i + step){ arr.push(i); } }else{ for(var i = start; i <= end;i = i + step){ arr.push(i); } } return arr; } console.log(range(1,10,2)); console.log(range(5,2,-1)); // 官方正解function range(start, end, step = start < end ? 1 : -1) { let array = []; if (step > 0) { for (let i = start; i <= end; i += step) array.push(i); } else { for (let i = start; i >= end; i += step) array.push(i); } return array;}function sum(array) { let total = 0; for (let value of array) { total += value; } return total;}// 逆转数组function reverseArray(arr){ var output = []; for(var i = arr.length-1;i >= 0;i--){ output.push(arr[i]); } return output;}console.log(reverseArray([1,2,3,4,5]));// 逆转数组2 function reverseArrayInPlace(array){ for(let i = 0; i < Math.floor(array.length / 2);i++){ let old = array[i]; array[i] = array[array.length -1-i]; array[array.length-1-i]= old; } return array; } console.log(reverseArrayInPlace([1,2,3,4,5]));// 实现列表var list = { value: 1, rest: { value: 2, rest: { value: 3, rest: null } }}function arrayToList(array) { let list = null; for (let i = array.length - 1; i >= 0; i--) { list = {value: array[i], rest: list}; } return list;}console.log(arrayToList([1,2,3]));// 列表转换成数组function listToArray(list) { let array = []; for (let node = list; node; node = node.rest) { array.push(node.value); } return array;}function listToArray(list){ let array = []; for(let node = list;node;node = node.rest){ array.push(node.value); } return array;}console.log(listToArray(list));// 创建一个新的列表function prepend(value,list) { return { value, rest: list };}// 返回列表中指定位置的元素function nth(list, n) { if (!list) return undefined; else if (n == 0) return list.value; else return nth(list.rest, n - 1);}function nth(list,n) { if(!list){ return undefined; }else if(n == 0){ return list.value; }else { return nth(list.rest,n-1); }}console.log(list,3);// 深度比较 编写一个函数deepEqual,接受两个参数,若两个对象是同一个值或两个对象中有相同属性,且使用deepEqual比较属性值均返回true时,返回truefunction deepEqual(a, b) { if (a === b) return true; if (a == null || typeof a != "object" || b == null || typeof b != "object") return false; let keysA = Object.keys(a), keysB = Object.keys(b); if (keysA.length != keysB.length) return false; for (let key of keysA) { if (!keysB.includes(key) || !deepEqual(a[key], b[key])) return false; } return true;}复制代码
第5章 高阶函数
让我们简单回顾一下前言当中的两个示例。其中第一个程序包含了6行代码并可以直接运行。
var total = 0,count = 1;while(count <= 10){ total += count; count += 1;}console.log(total);复制代码
第二个程序则依赖于外部函数才能执行,且只有一行代码。
console.log(sum(range(1,10)));复制代码
第二个程序编写的代码很好地表达了我们期望解决的问题。相比于将这些代码直接写到一起,这种表述方式更为简单,同时也易于避免错误。
5.1 抽象
在程序设计中,我们把这种编写代码的方式称为抽象。抽象可以隐藏底层的实现细节,从更高(或更加抽象)的层次看待我们要解决的问题。
作为一名程序员,我们需要具备在恰当时候将代码抽象出来,形成一个新的函数或概念的能力。
5.2 数组遍历抽象
// 1.将数组中的每个元素打印到控制台var array = [1,2,3];for(var i = 0; i < array.length;i++){ var current = array[i]; console.log(current);}// 2.将1抽象成一个函数function logEach(array){ for(var i = 0; i < array.length;i++){ console.log(array[i]);}}function forEach(array,action){ for(var i = 0; i < array.length;i++){ action(array[i]);}}forEach(["Wampeter","Foma","Granfalloon"],console.log);// 3.通常来说,我们不会给forEach传递一个预定义的函数,而是直接新建一个函数值。var numbers = [1,2,3,4,5],sum = 0;forEach(numbers,function(number){ sum += number;});console.log(sum);复制代码
实际上,我们不需要自己编写forEach函数,该函数其实是数组的一个标准方法。
function gatherCorrelations(journal) { var phis = {}; for(var entry = 0;entry < journal.length;entry++){ var events = journal[entry].events; for(var i = 0; i < events.length;i++){ var event = events[i]; if(!(event in phis)){ phis[event] = phi(tableFor(event,journal)); } } } return phis;}// 使用forEach改写上面的代码function gatherCorrelations(journal){ var phis = {}; journal.forEach(function(entry){ entry.events.forEach(function (event){ if(!(event in phis)){ phis[event] = phi(tableFor(event,journal)); } }) }) return phis;}复制代码
5.3 高阶函数
如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,那么我们可以将其称为高阶函数。
我们可以使用高阶函数对一系列操作和值进行抽象。
// 使用高阶函数新建另一些函数function greaterThan(n) { return function(m){ return m > n; }}var greaterThan10 = greaterThan(10);console.log(greaterThan10(11)); // true// 使用高阶函数来修改其他函数function noisy(f) { return function(arg){ console.log("calling with",arg); var val = f(arg); console.log("called with",arg,"-got",val); return val; }}noisy(Boolean)(0);// calling with 0// called with 0 -got false// 使用高阶函数来实现新的控制流function unless(test,then) { if(!test){ then(); }}function repeat(times,body){ for(var i = 0;i < times;i++){ body(i); }}repeat(3,function(n){ unless(n % 2,function (){ console.log(n,"is even"); })})复制代码
5.4 参数传递
我们在前面定义的函数noisy会把参数传递给另一个函数使用,这会导致一个相当严重的问题。
function noisy(f) { return function(arg){ console.log("calling with",arg); var val = f(arg); console.log("called with",arg,"-got",val); return val; }}复制代码
如果函数f接受多个参数,那么该函数只能接受第一个参数。函数f没有办法知道调用者传递给noisy的参数个数。
JavaScript函数的apply方法可以解决这个问题。
function transparentWrapping(f){ return function(){ return f.apply(null,arguments); }}复制代码
这里的transparentWrapping函数没有什么实际用处,但该函数返回的内部函数可以将用户指定的参数全部传递给f。
5.5 JSON
[{ "name": "Emma de Milliano", "sex": "f", "born": 1876, "died": 1956, "father": "Petrus de Milliano", "mother": "Sophia van Damme"},{ "name": "Carolus", "sex": "m", "born": 1832, "died": 1905, "father": "Carel Haverbeke", "mother": "Maria van Brussel"}... and so on]复制代码
这种格式是JSON格式,即JavaScript Object Notation的缩写。该格式广泛用于数据存储和web通信。
所有属性名都必须用双引号括起来,而且只能使用简单的数据表达式,不能填写函数调用、变量以及任何含有实际计算过程的代码。
JavaScript提供了JSON.stringify函数,用于将数据转换成该格式。还有JSON.parse函数,用于将该格式数据转换成原有的数据类型。
var string = JSON.stringify({ name: "X",born: 1980});console.log(string); // { "name":"X","born":1980}console.log(JSON.parse(string).born); // 1980复制代码
5.6 数组过滤
function filter(array,test){ var passed = []; for(var i = 0; i < array.length;i++){ if(test(array[i])){ passed.push(array[i]); } } return passed;}console.log(filter(ancestry,function(person) { return person.born > 1990 && person.born < 1925}))复制代码
该函数使用test函数作为参数来实现过滤操作。我们对数组中的每个元素调用test函数,并通过返回值来确定当前元素是否满足条件。
与forEach一样,filter函数也是数组中提供的一个标准方法。本例中定义的函数只是用于展示内部实现原理。
console.log(ancestry.filter(function (person){ return person.father == "Carel Haverbeke";}))复制代码
5.7 使用map函数转换数组
map方法可以对数组中的每个元素调用函数,然后利用返回值来构建一个新的数组,实现转换数组的操作。
function map(array,transform){ var mapped = []; for(var i = 0;i < array.length;i++){ mapped.push(transform(array[i]); } return mapped;}var overNinety = ancestry.filter(function(person){ return person.died - person.born > 90;})console.log(map(overNinety,function(person){ return person.name;}))复制代码
与forEach和filter一样,map也是数组中的一个标准方法。
5.8 使用reduce进行数据汇总
根据整个数组计算出一个值。
reduce函数包含三个参数:数组、执行合并操作的函数和初始值。
function reduce(array,combine,start) { var current = start; for(var i = 0;i < array.length;i++){ current = combine(current,array[i]); } return current;}console.log(reduce([1,2,3,4],function(a,b){ return a+b;},0))复制代码
数组中有一个标准的reduce方法,当然和我们上面看到的那个函数一致,可以简化合并操作。
console.log(ancestry.reduce(function(min,cur){ if(cur.born < min.born){ return cur; }else{ return min; }}))复制代码
5.9 可组合性
// 在不使用高阶函数的情况下,实现以上示例var min = ancestry[0];for(var i = 1;i < ancestry.length;i++){ var cur = ancestry[i]; if(cur.born < min.born){ min = cur; }}console.log(min);复制代码
这段代码中多了一些变量,虽然多了两行代码,但代码逻辑还是很容易让人理解的。
当你遇到需要组合函数的情况时,高阶函数的价值就突显出来了。举个例子,编写一段代码,找出数据集中男人和女人的平均年龄。
function average(array) { function plus(a,b){ return a+b; } return array.reduce(plus) / array.length;}function age(p){ return p.died -p.born;}function male(p){ return p.sex == "m";}function female(p){ return p.sex == "f";}console.log(average(ancestry.filter(male).map(age)));console.log(average(ancestry.filter(female).map(age)));复制代码
这段代码并没有将逻辑放到整个循环体中,而是将逻辑巧妙地组合成了我们所关注的几个方面:判断性别、计算年龄和计算平均数。
我们可以采用这种方式编写出逻辑清晰的代码。不过,编写这样的代码也是有代价的。
5.10 性能开销
将函数传递给forEach来处理数组迭代任务的确十分方便而且易于阅读。但JavaScript中函数调用却比简单的循环结构代价更高。
当程序执行很慢的时候,问题往往只是嵌套最深的循环体中的一小部分代码引起的。
5.11 曾曾曾曾......祖父
// 构建一个对象,将祖辈的姓名与表示人的对象关联起来var byName = {};ancestry.forEach(function(person){ byName[person.name] = person;})console.log(byName["Philibert Haverbeke"]);// 编写reduceAncestors函数,用于从家谱树中提炼出一个值。function reduceAncestors(person,f,defaultValue){ function valueFor(person){ if(person == null){ return defaultValue; }else{ return f(person,valueFor(byName[person.mother]),valueFor(byName[person.father])); } } return valueFor(person);}function sharedDNA(person,formMother,formFather){ if(person.name == "Pauwels van Haverbeke"){ return 1; }else{ return (formMother + formFather) / 2; }}var ph = byName["Philibert Haverbeke"];console.log(reduceAncestors(ph,sharedDNA,0) / 4);// 找出满足特定条件的祖先比例,比如可以查找年龄超过70岁的人function countAncestors(person,test){ function combine(person,formMother,formFather){ var thisOneCounts = test(person); return fromMather + formFather + (thisOneCounts ? 1 : 0); } return reduceAncestors(person,combine,0);}function longLivingPercentage(person){ var all = countAncestors(person,function(person){ return true; }) var longLiving = countAncestors(person,function(person){ return (person.died - person.born) >= 70; }) return longLiving / all;}console.log(longLivingPercentage(byName(["Emile Haverbeke"]));复制代码
5.12 绑定
每个函数都有一个bind方法,该方法可以用来创建新的函数,称为绑定函数。
var theSet = ["Carel Haverbeke","Maria van Brussel","Donald Duke"];function isInSet(set,person){ return set.indexOf(person.name) > -1;}console.log(ancestry.filter(function(person){ return isInset(theSet,person);}))console.log(ancestry.filter(isInSet.bind(null,theSet))); // same result复制代码
调用bind会返回一个新的函数,该函数调用isInSet时会将theSet作为第一个参数,并将传递给该函数的剩余参数一起传递给isInSet。
5.13 本章小结
将函数类型的值传递给其他函数不仅十分有用,而且还是JavaScript中一个重要的功能。我们可以在编写函数的时候把某些特定的操作预留出来,并在真正的函数调用中将具体的操作作为函数传递进来,实现完整的计算过程。
数组中提供了很多实用的高阶函数,其中forEach用于遍历数组元素,实现某些特定的功能。filter用于过滤掉一些元素,构造一个新数组。map会构建一个新数组,并通过一个函数处理每个元素,将处理结果放入新数组中。reduce则将数组元素最终归纳成一个值。
函数对象有一个apply方法,我们可以通过该方法调用函数,并使用数组来指定函数参数。另外还有一个bind方法,它用于创建一个新函数,并预先确定其中一部分参数。
5.14 习题
// 1.数组降维// 结合使用reduce与concat方法,将输入的二维数组(数组的数组)中的元素提取出来,并存放到一个一维数组中var arrays = [[1, 2, 3], [4, 5], [6]];console.log(arrays.reduce(function(flat, current) { return flat.concat(current);}, []));// 2.计算母子年龄差function average(array) { function plus(a, b) { return a + b; } return array.reduce(plus) / array.length;}var byName = {};ancestry.forEach(function(person) { byName[person.name] = person;});var differences = ancestry.filter(function(person) { return byName[person.mother] != null;}).map(function(person) { return person.born - byName[person.mother].born;});console.log(average(differences));// 3.计算平均寿命function average(array) { function plus(a, b) { return a + b; } return array.reduce(plus) / array.length;}function groupBy(array, groupOf) { var groups = {}; array.forEach(function(element) { var groupName = groupOf(element); if (groupName in groups) groups[groupName].push(element); else groups[groupName] = [element]; }); return groups;}var byCentury = groupBy(ancestry, function(person) { return Math.ceil(person.died / 100);});for (var century in byCentury) { var ages = byCentury[century].map(function(person) { return person.died - person.born; }); console.log(century + ": " + average(ages));}// 4.使用every和some方法function every(array, predicate) { for (var i = 0; i < array.length; i++) { if (!predicate(array[i])) return false; } return true;}function some(array, predicate) { for (var i = 0; i < array.length; i++) { if (predicate(array[i])) return true; } return false;}console.log(every([NaN, NaN, NaN], isNaN));// → trueconsole.log(every([NaN, NaN, 4], isNaN));// → falseconsole.log(some([NaN, 3, 4], isNaN));// → trueconsole.log(some([2, 3, 4], isNaN));// → false复制代码
第6章 深入理解对象
6.1 历史
6.2 方法
方法只是引用了函数值的属性。以下是一个简单的方法:
var rabbit = {};rabbit.speak = function(line){ console.log("The rabbit says '" + line + "'");}rabbit.speak("I'm alive."); // The rabbit says 'I'm alive.'复制代码
方法通常会在对象被调用时执行一些操作。将函数作为对象的方法调用时,会找到对象中对应的属性并直接调用。在调用object.method()时,对象中的一个特殊变量this会指向当前方法所属的对象。
function speak(line){ console.log("The " + this.type + " rabbit says '" + line + "'");}var whiteRabbit = { type: "white",speak: speak };var fatRabbit = { type: "fat",speak: speak };whiteRabbit.speak("Oh my ears and whiskers, " + "how late it's getting!");fatRabbit.speak("I could sure use a carrot right now.");// The white rabbit says 'Oh my ears and whiskers, how late it's getting!'// The fat rabbit says 'I could sure use a carrot right now.'复制代码
这段代码使用了关键字this来输出正在说话的兔子的种类。我们回想一下apply和bind方法,这两个方法接受的第一个参数可以用来模拟对象中方法的调用。这两个方法会把第一个参数复制给this。
函数有一个call方法,类似于apply方法。该方法也可以用于函数调用,但该方法会像普通函数一样接受参数,我们不需要将参数放到数组中。和apply和bind方法一样,你也可以向call方法传递一个特定的this值。
speak.apply(fatRabbit,["Burp!"]); // The fat rabbit says 'Burp!'speak.call({ type: "old"},"Oh my."); // The old rabbit says 'Oh my.'复制代码
6.3 原型
我们来仔细看看以下这段代码。
var empty = {};console.log(empty.toString); // ƒ toString() { [native code] }console.log(empty.toString()); // [object Object]复制代码
每个对象除了拥有自己的属性外,几乎都包含一个原型(prototype)。原型是另一个对象,是对象的一个属性来源。当开发人员访问一个对象不包含的属性时,就会从对象原型中搜索属性,接着是原型的原型,以此类推。
那么空对象的原型是什么呢?是Object.prototype,它是所有对象中原型的父原型。
console.log(Object.getPrototypeOf({}) == Object.prototype); // trueconsole.log(Object.getPrototypeOf(Object.prototype)); // null复制代码
JavaScript对象原型的关系是一种树形结构,整个树形结构的根部就是Object.prototype。Object.prototype提供了一些可以在所有对象中使用的方法。比如说,toString方法可以将一个对象转换成其字符串表示形式。
许多对象并不直接将Object.prototype作为其原型,而会使用另一个原型对象,用于提供对象自己的默认属性。函数继承自Function.prototype,而数组继承自Array.prototype。
console.log(Object.getPrototypeOf(isNaN) == Function.prototype); // trueconsole.log(Object.getPrototypeOf([]) == Array.prototype); // true复制代码
对于这样的原型对象来说,其自身也包含了一个原型对象,通常情况下是Object.prototype,所以说,这些原型对象可以间接提供toString这样的方法。
var protoRabbit = { speak: function(line){ console.log("The " + this.type + " rabbit says '" + line +"'"); }}var killerRabbit = Object.create(protoRabbit);killerRabbit.type = "killer";killerRabbit.speak("SKREEEE!"); // The killerrabbit says 'SKREEEE!'复制代码
原型对象protoRabbit是一个容器,用于包含所有兔子对象的公有属性。每个独立的兔子对象(比如killerRabbit)可以包含其自身属性(比如本例中的type属性),也可以派生其原型对象中公有的属性。
6.4 构造函数
在JavaScript中,调用函数之前添加一个关键字new则表示调用其构造函数。构造函数中包含了指向新对象的变量this,除非构造函数显式地返回了另一个对象的值,否则构造函数会返回这个新创建的对象。
通过关键字new创建的对象称之为构造函数的实例。
这里给出一个简单的用于创建rabbit的构造函数。构造函数的名称一般以大写字母开头。
function Rabbit(type){ this.type = type;}var killerRabbit = new Rabbit("killer");var blackRabbit = new Rabbit("black");console.log(blackRabbit.type); // black复制代码
对于构造函数来说(实际上,对所有函数适用),都会自动获得一个名为prototype的属性。在默认情况下,该属性是一个普通的派生自Object.prototype的空对象。所有使用特定构造函数创建的对象都会将构造函数的prototype属性作为其原型。因此,我们可以很容易地为所有使用Rabbit构造函数创建的对象添加speak方法。
Rabbit.prototype.speak = function(line){ console.log("The " + this.type + " rabbit says '" + line +"'");}blackRabbit.speak("Doom...");复制代码
构造函数其实就是函数,因此其实际原型是Function.prototype。而构造函数的prototype属性则是其所创建的实例的原型,而非构造函数自身的原型。
6.5 覆盖继承的属性
当你向一个对象添加属性时,无论该属性是否已经存在于对象原型中,该属性都会被添加到这个对象中去,并作为对象自己的属使用。如果原型中存在同名属性,那么在调用该属性时,就不会再调用原型中的那个属性了,转而调用我们添加到对象中的属性。但原型本身不会被修改。
Rbbit.prototype.teeth = "small";console.log(killerRabbit.teeth); // smallkillerRabbit.teeth = "long, sharp, and bloody";console.log(blackRabbit.teeth); // smallconsole.log(killerRabbit.teeth); // long, sharp, and bloodyconsole.log(Rabbit.prototype.teeth); // small复制代码
覆盖原型中存在的属性是很有用的一个特性。
我们也可以为标准函数和数组原型提供一个不同于Object原型的toString方法。
console.log(Array.prototype.toString == Object.prototype.toString); // falseconsole.log([1,2].toString()); // 1,2复制代码
直接使用数组调用Object.prototype.toString则会产生一个完全不同的字符串。
console.log(Object.prototype.toString.call([1,2])); // [object Array] ps:检测对象类型的最佳方式复制代码
6.6 原型污染
我们随时都可以使用原型对象添加新的属性和方法。
Rabbit.prototype.dance = function(){ console.log("The " + this.type + " rabbit dances a jig.");}killerRabbit.dance(); // The killer rabbit dances a jig.复制代码
回顾第4章中的示例:
var map = {};function storePhi(event,phi){ map[event] = phi;}storePhi("pizza",0.069);storePhi("touched tree",-0.081);复制代码
我们可以使用for/in循环遍历对象中所有的phi系数,并使用in操作符测试对象是否包含对应的属性。但不幸的是,这种方式会到对象的原型中寻找属性。
Object.prototype.nonsense = "hi";for(var name in map){ console.log(name);}// pizza// touched tree// nonsenseconsole.log("nonsense" in map); // trueconsole.log("toString" in map); // true复制代码
toString并没有出现在for/in循环中,而使用in运算符测试时则返回true。这是因为JavaScript会区分“可枚举(enumerable)”与“不可枚举(nonenumerable)”属性。
我们创建并赋予对象的所有属性都是可枚举的。而Object.prototype中的标准属性都不可枚举,因此这些标准属性不会出现在for/in循环中。
我们可以使用Object.defineProperty函数定义自己的不可枚举属性。
Object.defineProperty(Object.prototype,"hiddenNonsense",{enumerable: false,value: "hi"})for(var name in map){ console.log(name); }// pizza// touched treeconsole.log(map.hiddenNonsense); // hi复制代码
常规的in运算符会认为Object.prototype中的属性存在于我们的对象中。而对象的hasOwnProperty方法会告知我们对象自身是否包含某个属性,而不会搜索其原型。
console.log(map.hasOwnProperty("toString")); // false复制代码
当你担心某些人(装载到你程序中的某些其他代码)会干扰基础对象的原型时,我建议这样使用for/in循环:
for(var name in map){ if(map.hasOwnProperty(name)){ // ... this is an own property }}复制代码
6.7 无原型对象
我们可以使用Object.create函数并根据特定原型来创建对象。你可以传递null作为原型,并创建一个无原型对象。
var map = Object.create(null);map["pizza"] = 0.069;console.log("toString" in map); // falseconsole.log("pizza" in map); // true复制代码
6.8 多态
当编写一段代码时,我们可以使用包含特定接口的对象进行工作。在这个例子中是toString方法,只要对象支持这些接口,我们就可以将这些对象插入代码中,并保证代码正常工作。
我们将这种技术称为多态(polymorphism)。虽然在整个过程中没有修改任何东西的形状,但我们还是这么称呼这种技术。我们可以利用多态来操作不同类型的值,只要这些值支持所需的接口即可。
6.9 绘制表格
我们来通过一个稍微复杂的例子来深入了解一下多态和面向对象的编程思想。我们编写一个程序,将一个由表格单元格组成的二维数组转化成字符串,该字符串包含了与二维数组对应且布局规整的表格,要求每一列笔直整齐,每一行也要保证对齐。
首先,程序会计算每列的最小宽度和每行的最大高度,并保存到数组中。变量rows是一个二维数组,其中的每个数组元素用来表示一个单元格组成的行。
function rowHeights(rows){ return rows.map(function(row){ return row.reduce(function(max,cell){ return Math.max(max,cell.minHeight()); },0); });}function colWidths(rows){ return rows[0].map(function(_,i){ return rows.reduce(function(max,row){ return Math.max(max,row[i].minWidth()); },0); });}复制代码
下面是绘制表格的代码:
function drawTable(rows) { var heights = rowHeights(rows); var widths = colWidths(rows); function drawLine(blocks,lineNo){ return blocks.map(function (block){ return block[lineNo] }).join(" "); } function drawRow(row,rowNum) { var blocks = row.map(function(_,lineNo){ return drawLine(blocks,lineNo); }).join("\n"); } return rows.map(drawRow).join("\n");}复制代码
现在,我们来编写用于创建文本单元格的构造函数,实现表格的单元格接口。
function repeat(string,times){ var result = ""; for(var i = 0; i < times; i++){ result += string; } return result;}function TextCell(text){ this.text = text.split("\n");}TextCell.prototype.minWidth = function (){ return this.text.reduce(function (width,line){ return Math.max(width,line.length); },0)}TextCell.prototype.minHeight = function (){ return this.text.length;}TextCell.prototype.draw = function(width,height){ var result = []; for(var i = 0;i < height;i++){ var line = this.text[i] || ""; result.push(line + repeat(" ",width - line.height)); } return result;}复制代码
让我们来使用编写好的程序来创建一个5*5的棋盘。
var rows = [];for(var i = 0; i < 5; i++){ var row = []; for(var j = 0; j < 5;j++){ if((j + i) % 2 == 0){ row.push(new TextCell("##")); }else{ row.push(new TextCell(" ")); } } rows.push(row);}console.log(drawTable(rows));复制代码
Getter与Setter
在对象中,get或set方法用于指定属性的读取函数和修改函数,读取或修改属性时会自动调用这些函数。
var pile = { elements: ["eggshell","orange peel","worm"], get height(){ return this.elements.length; }, set height(value){ console.log("Ignoring attempt to set height to",value"); }}console.log(pile.height); // 3pile.height = 100; // Ignoring attempt to set height to",value复制代码
6.11 继承
每天不定时更新~,欢迎批评指正~