三、函数

原文:Functions

译者:飞龙

协议:CC BY-NC-SA 4.0

自豪地采用谷歌翻译

部分参考了《JavaScript 编程精解(第 2 版)》

人们认为计算机科学是天才的艺术,但是实际情况相反,只是许多人在其它人基础上做一些东西,就像一面由石子垒成的墙。

高德纳

函数是 JavaScript 编程的面包和黄油。 将一段程序包装成值的概念有很多用途。 它为我们提供了方法,用于构建更大程序,减少重复,将名称和子程序关联,以及将这些子程序相互隔离。

函数最明显的应用是定义新词汇。 用散文创造新词汇通常是不好的风格。 但在编程中,它是不可或缺的。

以英语为母语的典型成年人,大约有 2 万字的词汇量。 很少有编程语言内置了 2 万个命令。而且,可用的词汇的定义往往比人类语言更精确,因此灵活性更低。 因此,我们通常会引入新的概念,来避免过多重复。

定义函数

函数定义是一个常规绑定,其中绑定的值是一个函数。 例如,这段代码定义了square,来引用一个函数,它产生给定数字的平方:

const square = function(x) {return x * x;
};console.log(square(12));
// → 144

函数使用以关键字function起始的表达式创建。 函数有一组参数(在本例中只有x)和一个主体,它包含调用该函数时要执行的语句。 以这种方式创建的函数的函数体,必须始终包在花括号中,即使它仅包含一个语句。

一个函数可以包含多个参数,也可以不含参数。在下面的例子中,makeNoise函数中没有包含任何参数,而power则使用了两个参数:

var makeNoise = function() {console.log("Pling!");
};makeNoise();
// → Pling!const power = function(base, exponent) {let result = 1;for (let count = 0; count < exponent; count++) {result *= base;}return result;
};console.log(power(2, 10));
// → 1024

有些函数会产生一个值,比如powersquare,有些函数不会,比如makeNoise,它的唯一结果是副作用。 return语句决定函数返回的值。 当控制流遇到这样的语句时,它立即跳出当前函数并将返回的值赋给调用该函数的代码。 不带表达式的return关键字,会导致函数返回undefined。 没有return语句的函数,比如makeNoise,同样返回undefined

函数的参数行为与常规绑定相似,但它们的初始值由函数的调用者提供,而不是函数本身的代码。

绑定和作用域

每个绑定都有一个作用域,它是程序的一部分,其中绑定是可见的。 对于在任何函数或块之外定义的绑定,作用域是整个程序 - 您可以在任何地方引用这种绑定。它们被称为全局的。

但是为函数参数创建的,或在函数内部声明的绑定,只能在该函数中引用,所以它们被称为局部绑定。 每次调用该函数时,都会创建这些绑定的新实例。 这提供了函数之间的一些隔离 - 每个函数调用,都在它自己的小世界(它的局部环境)中运行,并且通常可以在不知道全局环境中发生的事情的情况下理解。

letconst声明的绑定,实际上是它们的声明所在的块的局部对象,所以如果你在循环中创建了一个,那么循环之前和之后的代码就不能“看见”它。JavaScript 2015 之前,只有函数创建新的作用域,因此,使用var关键字创建的旧式绑定,在它们出现的整个函数中内都可见,或者如果它们不在函数中,在全局作用域可见。

let x = 10;
if (true) {let y = 20;var z = 30;console.log(x + y + z);// → 60
}
// y is not visible here
console.log(x + z);
// → 40

每个作用域都可以“向外查看”它周围的作用域,所以示例中的块内可以看到x。 当多个绑定具有相同名称时例外 - 在这种情况下,代码只能看到最内层的那个。 例如,当halve函数中的代码引用n时,它看到它自己的n,而不是全局的n

const halve = function(n) {return n / 2;let n = 10;
console.log(halve(100));
// → 50
console.log(n);
// → 10

嵌套作用域

JavaScript 不仅区分全局和局部绑定。 块和函数可以在其他块和函数内部创建,产生多层局部环境。

例如,这个函数(输出制作一批鹰嘴豆泥所需的配料)的内部有另一个函数:

const hummus = function(factor) {const ingredient = function(amount, unit, name) {let ingredientAmount = amount * factor;if (ingredientAmount > 1) {unit += "s";}console.log(`${ingredientAmount} ${unit} ${name}`);};ingredient(1, "can", "chickpeas");ingredient(0.25, "cup", "tahini");ingredient(0.25, "cup", "lemon juice");ingredient(1, "clove", "garlic");ingredient(2, "tablespoon", "olive oil");ingredient(0.5, "teaspoon", "cumin");
};

ingredient函数中的代码,可以从外部函数中看到factor绑定。 但是它的局部绑定,比如unitingredientAmount,在外层函数中是不可见的。

简而言之,每个局部作用域也可以看到所有包含它的局部作用域。 块内可见的绑定集,由这个块在程序文本中的位置决定。 每个局部作用域也可以看到包含它的所有局部作用域,并且所有作用域都可以看到全局作用域。 这种绑定可见性方法称为词法作用域。

作为值的函数

函数绑定通常只充当程序特定部分的名称。 这样的绑定被定义一次,永远不会改变。 这使得容易混淆函数和名称。

let launchMissiles = function(value) {missileSystem.launch("now");
};
if (safeMode) {launchMissiles = function() {/* do nothing */};
}

在第 5 章中,我们将会讨论一些高级功能:将函数类型的值传递给其他函数。

符号声明

创建函数绑定的方法稍短。 当在语句开头使用function关键字时,它的工作方式不同。

function square(x) {return x * x;
}

这是函数声明。 该语句定义了绑定square并将其指向给定的函数。 写起来稍微容易一些,并且在函数之后不需要分号。

这种形式的函数定义有一个微妙之处。

console.log("The future says:", future());function future() {return "You'll never have flying cars";
}

前面的代码可以执行,即使在函数定义在使用它的代码下面。 函数声明不是常规的从上到下的控制流的一部分。 在概念上,它们移到了其作用域的顶部,并可被该作用域内的所有代码使用。 这有时是有用的,因为它以一种看似有意义的方式,提供了对代码进行排序的自由,而无需担心在使用之前必须定义所有函数。

箭头函数

函数的第三个符号与其他函数看起来有很大不同。 它不使用function关键字,而是使用由等号和大于号组成的箭头(=>)(不要与大于等于运算符混淆,该运算符写做>=)。

const power = (base, exponent) => {let result = 1;for (let count = 0; count < exponent; count++) {result *= base;}return result;
};

箭头出现在参数列表后面,然后是函数的主体。 它表达了一些东西,类似“这个输入(参数)产生这个结果(主体)”。

如果只有一个参数名称,则可以省略参数列表周围的括号。 如果主体是单个表达式,而不是大括号中的块,则表达式将从函数返回。 所以这两个square的定义是一样的:

const square1 = (x) => { return x * x; };
const square2 = x => x * x;

当一个箭头函数没有参数时,它的参数列表只是一组空括号。

const horn = () => {console.log("Toot");
};

在语言中没有很好的理由,同时拥有箭头函数和函数表达式。 除了我们将在第 6 章中讨论的一个小细节外,他们实现相同的东西。 在 2015 年增加了箭头函数,主要是为了能够以简短的方式编写小函数表达式。 我们将在第 5 章中使用它们。

调用栈

控制流经过函数的方式有点复杂。 让我们仔细看看它。 这是一个简单的程序,它执行了一些函数调用:

function greet(who) {console.log("Hello " + who);
}
greet("Harry");
console.log("Bye");

这个程序的执行大致是这样的:对greet的调用使控制流跳转到该函数的开始(第 2 行)。 该函数调用控制台的console.log来完成它的工作,然后将控制流返回到第 2 行。 它到达greet函数的末尾,所以它返回到调用它的地方,这是第 4 行。 之后的一行再次调用console.log。 之后,程序结束。

我们可以使用下图表示出控制流:

not in functionin greetin console.login greet
not in functionin console.log
not in function

由于函数在返回时必须跳回调用它的地方,因此计算机必须记住调用发生处上下文。 在一种情况下,console.log完成后必须返回greet函数。 在另一种情况下,它返回到程序的结尾。

计算机存储此上下文的地方是调用栈。 每次调用函数时,当前上下文都存储在此栈的顶部。 当函数返回时,它会从栈中删除顶部上下文,并使用该上下文继续执行。

存储这个栈需要计算机内存中的空间。 当栈变得太大时,计算机将失败,并显示“栈空间不足”或“递归太多”等消息。 下面的代码通过向计算机提出一个非常困难的问题来说明这一点,这个问题会导致两个函数之间的无限的来回调用。 相反,如果计算机有无限的栈,它将会是无限的。 事实上,我们将耗尽空间,或者“把栈顶破”。

function chicken() {return egg();
}
function egg() {return chicken();
}
console.log(chicken() + " came first.");
// → ??

可选参数

下面的代码可以正常执行:

function square(x) { return x * x; }
console.log(square(4, true, "hedgehog"));
// → 16

我们定义了square,只带有一个参数。 然而,当我们使用三个参数调用它时,语言并不会报错。 它会忽略额外的参数并计算第一个参数的平方。

JavaScript 对传入函数的参数数量几乎不做任何限制。如果你传递了过多参数,多余的参数就会被忽略掉,而如果你传递的参数过少,遗漏的参数将会被赋值成undefined

该特性的缺点是你可能恰好向函数传递了错误数量的参数,但没有人会告诉你这个错误。

优点是这种行为可以用于使用不同数量的参数调用一个函数。 例如,这个minus函数试图通过作用于一个或两个参数,来模仿-运算符:

function minus(a, b) {if (b === undefined) return -a;else return a - b;
}console.log(minus(10));
// → -10
console.log(minus(10, 5));
// → 5

如果你在一个参数后面写了一个=运算符,然后是一个表达式,那么当没有提供它时,该表达式的值将会替换该参数。

例如,这个版本的power使其第二个参数是可选的。 如果你没有提供或传递undefined,它将默认为 2,函数的行为就像square

function power(base, exponent = 2) {let result = 1;for (let count = 0; count < exponent; count++) {result *= base;}return result;
}console.log(power(4));
// → 16
console.log(power(2, 6));
// → 64

在下一章当中,我们将会了解如何获取传递给函数的整个参数列表。我们可以借助于这种特性来实现函数接收任意数量的参数。比如console.log就利用了这种特性,它可以用来输出所有传递给它的值。

console.log("C", "O", 2);
// → C O 2

闭包

函数可以作为值使用,而且其局部绑定会在每次函数调用时重新创建,由此引出一个值得我们探讨的问题:如果函数已经执行结束,那么这些由函数创建的局部绑定会如何处理呢?

下面的示例代码展示了这种情况。代码中定义了函数wrapValue,该函数创建了一个局部绑定localVariable,并返回一个函数,用于访问并返回局部绑定localVariable

function wrapValue(n) {let local = n;return () => local;
}let wrap1 = wrapValue(1);
let wrap2 = wrapValue(2);
console.log(wrap1());
// → 1
console.log(wrap2());
// → 2

这是允许的并且按照您的希望运行 - 绑定的两个实例仍然可以访问。 这种情况很好地证明了一个事实,每次调用都会重新创建局部绑定,而且不同的调用不能覆盖彼此的局部绑定。

这种特性(可以引用封闭作用域中的局部绑定的特定实例)称为闭包。 引用来自周围的局部作用域的绑定的函数称为(一个)闭包。 这种行为不仅可以让您免于担心绑定的生命周期,而且还可以以创造性的方式使用函数值。

我们对上面那个例子稍加修改,就可以创建一个可以乘以任意数字的函数。

function multiplier(factor) {return number => number * factor;
}let twice = multiplier(2);
console.log(twice(5));
// → 10

由于参数本身就是一个局部绑定,所以wrapValue示例中显式的local绑定并不是真的需要。

考虑这样的程序需要一些实践。 一个好的心智模型是,将函数值看作值,包含他们主体中的代码和它们的创建位置的环境。 被调用时,函数体会看到它的创建环境,而不是它的调用环境。

这个例子调用multiplier并创建一个环境,其中factor参数绑定了 2。 它返回的函数值,存储在twice中,会记住这个环境。 所以当它被调用时,它将它的参数乘以 2。

递归

一个函数调用自己是完全可以的,只要它没有经常这样做以致溢出栈。 调用自己的函数被称为递归函数。 递归允许一些函数以不同的风格编写。 举个例子,这是power的替代实现:

function power(base, exponent) {if (exponent == 0) {return 1;} else {return base * power(base, exponent - 1);}
}console.log(power(2, 3));
// → 8

这与数学家定义幂运算的方式非常接近,并且可以比循环变体将该概念描述得更清楚。 该函数以更小的指数多次调用自己以实现重复的乘法。

但是这个实现有一个问题:在典型的 JavaScript 实现中,它大约比循环版本慢三倍。 通过简单循环来运行,通常比多次调用函数开销低。

速度与优雅的困境是一个有趣的问题。 您可以将其视为人性化和机器友好性之间的权衡。 几乎所有的程序都可以通过更大更复杂的方式加速。 程序员必须达到适当的平衡。

power函数的情况下,不雅的(循环)版本仍然非常简单易读。 用递归版本替换它没有什么意义。 然而,通常情况下,一个程序处理相当复杂的概念,为了让程序更直接,放弃一些效率是有帮助的。

担心效率可能会令人分心。 这又是另一个让程序设计变复杂的因素,当你做了一件已经很困难的事情时,担心的额外事情可能会瘫痪。

因此,总是先写一些正确且容易理解的东西。 如果您担心速度太慢 - 通常不是这样,因为大多数代码的执行不足以花费大量时间 - 您可以事后进行测量并在必要时进行改进。

递归并不总是循环的低效率替代方法。 递归比循环更容易解决解决一些问题。 这些问题通常是需要探索或处理几个“分支”的问题,每个“分支”可能再次派生为更多的分支。

考虑这个难题:从数字 1 开始,反复加 5 或乘 3,就可以产生无限数量的新数字。 你会如何编写一个函数,给定一个数字,它试图找出产生这个数字的,这种加法和乘法的序列?

例如,数字 13 可以通过先乘 3 然后再加 5 两次来到达,而数字 15 根本无法到达。

使用递归编码的解决方案如下所示:

function findSolution(target) {function find(current, history) {if (current == target) {return history;} else if (current > target) {return null;} else {return find(current + 5, `(${history} + 5)`) ||find(current * 3, `(${history} * 3)`);}}return find(1, "1");
}console.log(findSolution(24));
// → (((1 * 3) + 5) * 3)

需要注意的是该程序并不需要找出最短运算序列,只需要找出任何一个满足要求的序列即可。

如果你没有看到它的工作原理,那也没关系。 让我们浏览它,因为它是递归思维的很好的练习。

内层函数find进行实际的递归。 它有两个参数:当前数字和记录我们如何到达这个数字的字符串。 如果找到解决方案,它会返回一个字符串,显示如何到达目标。 如果从这个数字开始找不到解决方案,则返回null

为此,该函数执行三个操作之一。 如果当前数字是目标数字,则当前历史记录是到达目标的一种方式,因此将其返回。 如果当前的数字大于目标,则进一步探索该分支是没有意义的,因为加法和乘法只会使数字变大,所以它返回null。 最后,如果我们仍然低于目标数字,函数会尝试从当前数字开始的两个可能路径,通过调用它自己两次,一次是加法,一次是乘法。 如果第一次调用返回非null的东西,则返回它。 否则,返回第二个调用,无论它产生字符串还是null

为了更好地理解函数执行过程,让我们来看一下搜索数字 13 时,find函数的调用情况:

find(1, "1")find(6, "(1 + 5)")find(11, "((1 + 5) + 5)")find(16, "(((1 + 5) + 5) + 5)")too bigfind(33, "(((1 + 5) + 5) * 3)")too bigfind(18, "((1 + 5) * 3)")too bigfind(3, "(1 * 3)")find(8, "((1 * 3) + 5)")find(13, "(((1 * 3) + 5) + 5)")found!

缩进表示调用栈的深度。 第一次调用find时,它首先调用自己来探索以(1 + 5)开始的解决方案。 这一调用将进一步递归,来探索每个后续的解,它产生小于或等于目标数字。 由于它没有找到一个命中目标的解,所以它向第一个调用返回null。 那里的||操作符会使探索(1 * 3)的调用发生。 这个搜索的运气更好 - 它的第一次递归调用,通过另一个递归调用,命中了目标数字。 最内层的调用返回一个字符串,并且中间调用中的每个“||”运算符都会传递该字符串,最终返回解决方案。

添加新函数

这里有两种常用的方法,将函数引入到程序中。

首先是你发现自己写了很多次非常相似的代码。 我们最好不要这样做。 拥有更多的代码,意味着更多的错误空间,并且想要了解程序的人资料。 所以我们选取重复的功能,为它找到一个好名字,并把它放到一个函数中。

第二种方法是,你发现你需要一些你还没有写的功能,这听起来像是它应该有自己的函数。 您将首先命名该函数,然后您将编写它的主体。 在实际定义函数本身之前,您甚至可能会开始编写使用该函数的代码。

给函数起名的难易程度取决于我们封装的函数的用途是否明确。对此,我们一起来看一个例子。

我们想编写一个打印两个数字的程序,第一个数字是农场中牛的数量,第二个数字是农场中鸡的数量,并在数字后面跟上CowsChickens用以说明,并且在两个数字前填充 0,以使得每个数字总是由三位数字组成。

007 Cows
011 Chickens

这需要两个参数的函数 - 牛的数量和鸡的数量。 让我们来编程。

function printFarmInventory(cows, chickens) {let cowString = String(cows);while (cowString.length < 3) {cowString = "0" + cowString;}console.log(`${cowString} Cows`);let chickenString = String(chickens);while (chickenString.length < 3) {chickenString = "0" + chickenString;}console.log(`${chickenString} Chickens`);
}
printFarmInventory(7, 11);

在字符串表达式后面写.length会给我们这个字符串的长度。 因此,while循环在数字字符串前面加上零,直到它们至少有三个字符的长度。

任务完成! 但就在我们即将向农民发送代码(连同大量发票)时,她打电话告诉我们,她也开始饲养猪,我们是否可以扩展软件来打印猪的数量?

当然没有问题。但是当再次复制粘贴这四行代码的时候,我们停了下来并重新思考。一定还有更好的方案来解决我们的问题。以下是第一种尝试:

function printZeroPaddedWithLabel(number, label) {let 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);

这种方法解决了我们的问题!但是printZeroPaddedWithLabel这个函数并不十分恰当。它把三个操作,即打印信息、数字补零和添加标签放到了一个函数中处理。

这一次,我们不再将程序当中重复的代码提取成一个函数,而只是提取其中一项操作。

function zeroPad(number, width) {let 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);

名为zeroPad的函数具有很好的名称,使读取代码的人更容易弄清它的功能。 而且这样的函数在更多的情况下是有用的,不仅仅是这个特定程序。 例如,您可以使用它来帮助打印精确对齐的数字表格。

我们的函数应该包括多少功能呢?我们可以编写一个非常简单的函数,只支持将数字扩展成 3 字符宽。也可以编写一个复杂通用的数字格式化系统,可以处理分数、负数、小数点对齐和使用不同字符填充等。

一个实用原则是不要故作聪明,除非你确定你会需要它。 为你遇到的每一个功能编写通用“框架”是很诱人的。 控制住那种冲动。 你不会完成任何真正的工作 - 你只会编写你永远不会使用的代码。

函数及其副作用

我们可以将函数分成两类:一类调用后产生副作用,而另一类则产生返回值(当然我们也可以定义同时产生副作用和返回值的函数)。

在农场案例当中,我们调用第一个辅助函数printZeroPaddedWithLabel来产生副作用,打印一行文本信息。而在第二个版本中有一个zeroPad函数,我们调用它来产生返回值。第二个函数比第一个函数的应用场景更加广泛,这并非偶然。相比于直接产生副作用的函数,产生返回值的函数则更容易集成到新的环境当中使用。

纯函数是一种特定类型的,生成值的函数,它不仅没有副作用,而且也不依赖其他代码的副作用,例如,它不读取值可能会改变的全局绑定。 纯函数具有令人愉快的属性,当用相同的参数调用它时,它总是产生相同的值(并且不会做任何其他操作)。 这种函数的调用,可以由它的返回值代替而不改变代码的含义。 当你不确定纯函数是否正常工作时,你可以通过简单地调用它来测试它,并且知道如果它在当前上下文中工作,它将在任何上下文中工作。 非纯函数往往需要更多的脚手架来测试。

尽管如此,我们也没有必要觉得非纯函数就不好,然后将这类函数从代码中删除。副作用常常是非常有用的。比如说,我们不可能去编写一个纯函数版本的console.log,但console.log依然十分实用。而在副作用的帮助下,有些操作则更易、更快实现,因此考虑到运算速度,有时候纯函数并不可取。

本章小结

本章教你如何编写自己的函数。 当用作表达式时,function关键字可以创建一个函数值。 当作为一个语句使用时,它可以用来声明一个绑定,并给它一个函数作为它的值。 箭头函数是另一种创建函数的方式。

// Define f to hold a function value
const f = function(a) {console.log(a + 2);
};// Declare g to be a function
function g(a, b) {return a * b * 3.5;
}// A less verbose function value
let h = a => a % 3;

理解函数的一个关键方面是理解作用域。 每个块创建一个新的作用域。 在给定作用域内声明的参数和绑定是局部的,并且从外部看不到。 用var声明的绑定行为不同 - 它们最终在最近的函数作用域或全局作用域内。

将程序执行的任务分成不同的功能是有帮助的。 你不必重复自己,函数可以通过将代码分组成一些具体事物,来组织程序。

习题

最小值

前一章介绍了标准函数Math.min,它可以返回参数中的最小值。我们现在可以构建相似的东西。编写一个函数min,接受两个参数,并返回其最小值。

// Your code here.console.log(min(0, 10));
// → 0
console.log(min(0, -10));
// → -10

递归

我们已经看到,%(取余运算符)可以用于判断一个数是否是偶数,通过使用% 2来检查它是否被 2 整除。这里有另一种方法来判断一个数字是偶数还是奇数:

  • 0是偶数

  • 1是奇数

  • 对于其他任何数字N,其奇偶性与N–2相同。

定义对应此描述的递归函数isEven。 该函数应该接受一个参数(一个正整数)并返回一个布尔值。

使用 50 与 75 测试该函数。想想如果参数为 –1 会发生什么以及产生相应结果的原因。请你想一个方法来修正该问题。

// Your code here.console.log(isEven(50));
// → true
console.log(isEven(75));
// → false
console.log(isEven(-1));
// → ??

字符计数

你可以通过编写"string"[N],来从字符串中得到第N个字符或字母。 返回的值将是只包含一个字符的字符串(例如"b")。 第一个字符的位置为零,这会使最后一个字符在string.length - 1。 换句话说,含有两个字符的字符串的长度为2,其字符的位置为 0 和 1。

编写一个函数countBs,接受一个字符串参数,并返回一个数字,表示该字符串中有多少个大写字母"B"

接着编写一个函数countChar,和countBs作用一样,唯一区别是接受第二个参数,指定需要统计的字符(而不仅仅能统计大写字母"B")。并使用这个新函数重写函数countBs

// Your code here.console.log(countBs("BBC"));
// → 2
console.log(countChar("kakkerlak", "k"));
// → 4

JavaScript 编程精解 中文第三版 三、函数相关推荐

  1. JavaScript 编程精解 中文第三版 零、前言

    零.前言 原文:Introduction 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScript 编程精解(第 2 版)> We think ...

  2. JavaScript 编程精解 中文第三版 翻译完成

    JavaScript 编程精解 中文第三版 原书:Eloquent JavaScript 3rd edition 译者:飞龙 自豪地采用谷歌翻译 部分参考了<JavaScript 编程精解(第 ...

  3. JavaScript 编程精解 中文第三版 二十一、项目:技能分享网站

    二十一.项目:技能分享网站 原文:Project: Skill-Sharing Website 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScri ...

  4. JavaScript 编程精解 中文第三版 十三、浏览器中的 JavaScript

    十三.浏览器中的 JavaScript 原文:JavaScript and the Browser 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaSc ...

  5. JavaScript 编程精解 中文第三版 十二、项目:编程语言

    十二.项目:编程语言 原文:Project: A Programming Language 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScript ...

  6. JavaScript 编程精解 中文第三版 六、对象的秘密

    六.对象的秘密 原文:The Secret Life of Objects 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScript 编程精解(第 ...

  7. JavaScript 编程精解 中文第三版 九、正则表达式

    九.正则表达式 原文:Regular Expressions 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScript 编程精解(第 2 版)> ...

  8. JavaScript 编程精解 中文第三版 二十、Node.js

    二十.Node.js 原文:Node.js 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScript 编程精解(第 2 版)> A stude ...

  9. JavaScript 编程精解 中文第三版 十九、项目:像素艺术编辑器

    十九.项目:像素艺术编辑器 原文:Project: A Pixel Art Editor 译者:飞龙 协议:CC BY-NC-SA 4.0 自豪地采用谷歌翻译 部分参考了<JavaScript ...

最新文章

  1. TensorFlow练习7: 基于RNN生成古诗词
  2. Pytorch多进程最佳实践
  3. Java中Collection集合接口
  4. 一篇文章让你轻松搞定SpringBoot和SpringCloud之间的版本选择!!!
  5. 查询优化器内核剖析第四篇:从一个实例看执行计划
  6. Python 第三方模块之 beautifulsoup(bs4)- 解析 HTML
  7. Failure to find com.oracle:ojdbc6:jar:11.2.0.1.0
  8. SIFT算法详解(二)
  9. Java里的阻塞队列
  10. Unity3D TextMeshPro
  11. 【电子技术实验设计】简易交通灯控制逻辑电路设计报告
  12. EPLAN如何保护电气图纸
  13. 基于raft协议的P2P下载器
  14. Python 内建函数大全
  15. cocos creator飞机大战总结
  16. Iceberg在袋鼠云的探索及实践
  17. android 查看cpu 工具6,Android 之CPU监控命令
  18. Android 9.0 SystemUI 下拉状态栏快捷开关
  19. android 三种定位方式
  20. iOS测试之接口测试总结

热门文章

  1. Http详解,2021年是做Android开发人员的绝佳时机
  2. MyDocument.exe病毒查杀方法
  3. sqlserver 查询练习
  4. iApp4Me一周年记
  5. 换分币c语言程序,编写程序输出用一元人民币兑换成1分、2分和5分硬币的不同兑换方法...
  6. 简单图形界面初学 :tkinter+阿里云接口+爬虫,实现全国天气查询
  7. 一键启动多应用(windows版)
  8. 1.1 数列极限与函数极限
  9. 京东码农:淡定认为裁员轮不到我!没想正讨论工作就接到被裁通知
  10. 想转SAP FICO顾问的必看(转)