重新介绍JavaScript(JS教程)
原文链接:https://developer.mozilla.org/en-US/docs/Web/JavaScript/A_re-introduction_to_JavaScript
为什么要重新介绍? 因为JavaScript因为是世界上最容易被误解的编程语言而臭名昭著。 它经常被嘲笑为玩具,但在其欺骗性简单的层面下,强大的语言功能等待着。 JavaScript现在被大量高端应用程序使用,这表明对该技术的深入了解对任何Web或移动开发人员来说都是一项重要技能。
从概述语言的历史开始是有用的。 JavaScript是由Brendan Eich于1995年创建的,当时他是Netscape的工程师。 JavaScript于1996年初首次与Netscape 2一起发布。它原本将被称为LiveScript,但它在一个命运多舛的营销决策中被重命名,试图利用Sun Microsystem的Java语言的流行 - 尽管两者几乎没有 共同的。 从那以后,这一直是混乱的根源。
几个月后,微软发布了带有Internet Explorer 3的JScript。这是一个兼容性很强的JavaScript工作。 几个月后,Netscape向欧洲标准组织Ecma International提交了JavaScript,并于当年推出了第一版ECMAScript标准。 该标准于1999年作为ECMAScript第3版获得了重大更新,从此以后它一直保持稳定。 由于语言复杂性的政治差异,第四版被放弃了。 第四版的许多部分构成了2009年12月发布的ECMAScript第5版以及2015年6月发布的第6版主要标准的基础。
因为它比较熟悉,所以从这一点开始我们将ECMAScript称为“JavaScript”。
与大多数编程语言不同,JavaScript语言没有输入或输出的概念。 它被设计为在主机环境中作为脚本语言运行,并且由主机环境提供与外部世界通信的机制。 最常见的主机环境是浏览器,但JavaScript解释器也可以在大量其他地方找到,包括Adobe Acrobat,Adobe Photoshop,SVG图像,Yahoo的Widget引擎,服务器端环境,如Node.js,NoSQL数据库 比如开源Apache CouchDB,嵌入式计算机,完整的桌面环境,如GNOME(GNU / Linux操作系统最流行的GUI之一)等等。
概览
JavaScript是一种多范式的动态语言,包含类型和运算符,标准内置对象和方法。 它的语法基于Java和C语言 - 这些语言中的许多结构也适用于JavaScript。 JavaScript支持使用对象原型的面向对象编程,而不是类(请参阅有关原型继承和ES2015类的更多信息)。 JavaScript还支持函数式编程 - 因为它们是对象,函数可以存储在变量中并像任何其他对象一样传递。
让我们从查看任何语言的构建块开始:类型。 JavaScript程序操纵值,这些值都属于一个类型。 JavaScript的类型是:
Number
String
Boolean
Function
Object
Symbol
(ES2015新特性)
…哦,还有undefined
和null
,它们…有点奇怪。还有Array
,这是一种特殊的对象。还有Date
和RegExp
,它们是免费获得的对象。 从技术上讲,函数只是一种特殊类型的对象。 所以类型图看起来更像是这样的:
Number
String
Boolean
Symbol
(ES2015新特性)Object
Function
Array
Date
RegExp
null
undefined
还有一些内置的Error
类型。 但是,如果我们坚持使用第一个图表,情况要容易得多,所以我们现在将讨论那里列出的类型。
Number
根据规范,JavaScript中的数字是“双精度64位格式IEEE 754值”。 这有一些有趣的后果。 在JavaScript中没有整数这样的东西,所以如果你习惯于用C或Java进行数学运算,你必须要小心算术。
另外,请注意以下内容:
0.1 + 0.2 == 0.30000000000000004;
实际上,整数值被视为32位整数,有些实现甚至以这种方式存储它们,直到它们被要求执行对数字有效但不对32位整数有效的指令。 这对于位操作非常重要。
支持标准算术运算符,包括加法,减法,模数(或余数)算术等。 还有一个我们之前没有提到的内置对象,叫做Math
,提供高级数学函数和常量:
Math.sin(3.5);
var circumference = 2 * Math.PI * r;
您可以使用内置的parseInt()
函数将字符串转换为整数。 这将转换的基础作为可选的第二个参数,您应该始终提供:
parseInt('123', 10); // 123
parseInt('010', 10); // 10
在较旧的浏览器中,以“0”开头的字符串假定为八进制(基数为8),但自2013年以来情况并非如此。 除非您确定自己的字符串格式,否则您可以在这些旧版浏览器上获得惊人的结果:
parseInt('010'); // 8
parseInt('0x10'); // 16
在这里,我们看到parseInt()
函数将第一个字符串视为八进制,因为前导0,而第二个字符串由于前导“0x”而被视为十六进制。 十六进制表示法仍然存在; 只有八进制被删除。
如果要将二进制数转换为整数,只需更改基数:
parseInt('11', 2); // 3
同样,您可以使用内置的parseFloat()
函数解析浮点数。 与parseInt()
不同,parseFloat()
总是使用基数10。
您还可以使用一元+
运算符将值转换为数字:
+ '42'; // 42
+ '010'; // 10
+ '0x10'; // 16
如果字符串是非数字的,则返回一个名为NaN
的特殊值(“非数字”的缩写):
parseInt('hello', 10); // NaN
NaN
是有毒的:如果你将它作为操作数提供给任何数学运算,结果也将是NaN
:
NaN + 5; // NaN
您可以使用内置的isNaN()
函数测试NaN
:
isNaN(NaN); // true
JavaScript还具有特殊值Infinity
和-Infinity
:
1 / 0; // Infinity
-1 / 0; // -Infinity
您可以使用内置的isFinite()
函数测试Infinity
,-Infinity
和NaN
值:
isFinite(1 / 0); // false
isFinite(-Infinity); // false
isFinite(NaN); // false
parseInt()
和parseFloat()
函数解析字符串,直到它们到达对指定数字格式无效的字符,然后返回解析到该点的数字。 但是,如果包含无效字符,“+”运算符只是将字符串转换为NaN。 只需在控制台中尝试使用每种方法解析字符串“10.2abc”,您就会更好地理解差异。
String
JavaScript中的字符串是Unicode字符序列。 对于那些不得不处理国际化问题的人来说,这应该是个好消息。 更准确地说,它们是UTF-16代码单元的序列; 每个代码单元由16位数字表示。 每个Unicode字符由1或2个代码单元表示。
如果要表示单个字符,只需使用由该单个字符组成的字符串即可。
要查找字符串的长度(以代码单位表示),请访问其length
属性:
'hello'.length; // 5
这是我们第一次使用JavaScript对象! 我们提到你也可以像对象一样使用字符串吗? 他们也有方法允许您操作字符串并访问有关字符串的信息:
'hello'.charAt(0); // "h"
'hello, world'.replace('world', 'mars'); // "hello, mars"
'hello'.toUpperCase(); // "HELLO"
其它类型
JavaScript区分null
,这是一个表示故意非值的值(并且只能通过null
关键字访问)以及undefined
,它是一个未定义类型的值,表示未初始化的变量 - 即还没有被指定的变量。 我们稍后将讨论变量,但在JavaScript中,可以声明变量而不为其赋值。 如果这样做,变量的类型是undefined
。 undefined
实际上是一个常数。
JavaScript有一个布尔类型,可能的值为true
和false
(两者都是关键字。)根据以下规则,任何值都可以转换为布尔值:
false
,0
,空字符串(""
),NaN
,null
以及undefined
都会变成false
- 所有其它值都会变成
true
您可以使用Boolean()
函数显式执行此转换:
Boolean(''); // false
Boolean(234); // true
但是,这很少是必要的,因为JavaScript会在需要布尔值时静默执行此转换,例如在if
语句中(见下文)。 出于这个原因,我们有时简单地说“真值”和“假值”,这意味着当转换为布尔值时,这些值分别变为true
和false
。 或者,这些值可分别称为“truthy”和“falsy”。
布尔运算,例如&&
(逻辑和),||
(逻辑或),和!
(逻辑非)支持; 见下文。
变量
JavaScript中的新变量使用以下三个关键字之一声明:let
,const
或var
。
let
允许您声明块级变量。 声明的变量可以从它所包含的块中获得。
let a;
let name = 'Simon';
以下是使用let
声明的变量的范围示例:
// myLetVariable is *not* visible out herefor (let myLetVariable = 0; myLetVariable < 5; myLetVariable++) {// myLetVariable is only visible in here
}// myLetVariable is *not* visible out here
const
允许您声明其值永远不会更改的变量。 该变量可从声明的块中获得。
const Pi = 3.14; // variable Pi is set
Pi = 1; // will throw an error because you cannot change a constant variable.
var
是最常见的声明性关键字。 它没有其他两个关键字的限制。 这是因为它传统上是在JavaScript中声明变量的唯一方法。 使用var
关键字声明的变量可以从声明它的函数中获得。
var a;
var name = 'Simon';
使用var
声明的变量的范围示例:
// myVarVariable *is* visible out herefor (var myVarVariable = 0; myVarVariable < 5; myVarVariable++) {// myVarVariable is visible to the whole function
}// myVarVariable *is* visible out here
如果声明变量而不为其赋值,则其类型是undefined
。
JavaScript和Java等其他语言之间的一个重要区别是,在JavaScript中,块没有范围; 只有函数有一个范围。 因此,如果在复合语句中使用var
定义变量(例如在if
控制结构中),则整个函数都可以看到它。 但是,从ECMAScript 2015
开始,let
和const
声明允许您创建块范围的变量。
操作符
JavaScript的数字运算符是+
, -
,*
,/
和%
,其中%
是余数运算符([与模数相同)。值使用=赋值,并且还有复合赋值语句,如+=
和-=
。 这些是x = x operator y
的扩展。
x += 5;
x = x + 5;
您可以使用++
和 --
分别递增和递减。 这些可以用作前缀或后缀运算符。
+ operator也能实现字符串连接:
'hello' + ' world'; // "hello world"
如果将字符串加到数字(或其他值),则首先将所有内容转换为字符串。 这可能会让你失望:
'3' + 4 + 5; // "345"
3 + 4 + '5'; // "75"
将空字符串添加到某个内容是将其转换为字符串本身的有用方法。
可以使用<
,>
,<=
和>=
在JavaScript中进行比较。 这些适用于字符串和数字。 等号有点不那么简单。 如果你给它不同的类型,双等运算符会执行类型强制转化,有时会产生有趣的结果:
123 == '123'; // true
1 == true; // true
要避免类型强制转化,请使用三重等号运算符:
123 === '123'; // false
1 === true; // false
也有!=
和!==
运算符。
JavaScript也有按位操作。 如果你想使用它们,它们就在那里。
控制结构
JavaScript与C系列中的其他语言具有类似的控制结构集。 if
和else
支持条件语句; 如果你愿意,你可以将它们连在一起:
var name = 'kittens';
if (name == 'puppies') {name += ' woof';
} else if (name == 'kittens') {name += ' meow';
} else {name += '!';
}
name == 'kittens meow';
JavaScript有while
循环和do-while
循环。 第一个是基本循环; 第二个是你希望确保循环体至少执行一次的循环:
while (true) {// an infinite loop!
}var input;
do {input = get_input();
} while (inputIsNotValid(input));
JavaScript的for loop与C和Java中的相同:它允许您在一行上提供循环的控制信息。
for (var i = 0; i < 5; i++) {// Will execute 5 times
}
JavaScript还包含另外两个突出的for循环:for … of
for (let value of array) {// do something with value
}
以及for …in
for (let property in object) {// do something with object property
}
&&
和||
运算符使用短路逻辑,这意味着它们是否将执行第二个操作数依赖于第一个操作数。 这对于在访问其属性之前检查空对象很有用:
var name = o && o.getName();
或者用于缓存值(当虚假值无效时):
var name = cachedName || (cachedName = getName());
JavaScript有条件表达式的三元运算符:
var allowed = (age > 18) ? 'yes' : 'no';
switch
语句可以基于数字或字符串用于多个分支:
switch (action) {case 'draw':drawIt();break;case 'eat':eatIt();break;default:doNothing();
}
如果你不添加break
语句,执行将“落到”下一级。 这很少是你想要的 - 事实上,如果你真的想要它来帮助调试,那么值得专门用标记来标记故意的评论:
switch (a) {case 1: // fallthroughcase 2:eatIt();break;default:doNothing();
}
default子句是可选的。 如果你愿意,你可以在switch部分和case中都有表达式; 使用===
运算符在两者之间进行比较:
switch (1 + 3) {case 2 + 2:yay();break;default:neverhappens();
}
对象
JavaScript对象可以被认为是名称 - 值对的简单集合。 因此,它们类似于:
- Python中的字典。
- Perl和Ruby中的哈希。
- C和C++中的哈希表。
- Java中的HashMaps。
- PHP中的关联数组。
这种数据结构如此广泛使用的事实证明了它的多功能性。 由于JavaScript中的所有内容(条形核心类型)都是对象,因此任何JavaScript程序自然都会涉及大量的哈希表查找。 他们这么快就是一件好事!
“name”部分是JavaScript字符串,而值可以是任何JavaScript值 - 包括更多对象。 这允许您构建任意复杂度的数据结构。
创建空对象有两种基本方法:
var obj = new Object();
以及
var obj = {};
这些在语义上是等价的; 第二种称为对象文字语法,更方便。 此语法也是JSON格式的核心,应始终是首选。
对象文字语法可用于完整地初始化对象:
var obj = {name: 'Carrot',for: 'Max', // 'for' is a reserved word, use '_for' instead.details: {color: 'orange',size: 12}
};
属性访问可以链接在一起:
obj.details.color; // orange
obj['details']['size']; // 12
以下示例创建一个对象原型,Person
和该原型的实例,you
。
function Person(name, age) {this.name = name;this.age = age;
}// Define an object
var you = new Person('You', 24);
// We are creating a new person named "You" aged 24.
创建后,可以通过以下两种方式之一再次访问对象的属性:
// dot notation
obj.name = 'Simon';
var name = obj.name;
以及…
// bracket notation
obj['name'] = 'Simon';
var name = obj['name'];
// can use a variable to define a key
var user = prompt('what is your key?')
obj[user] = prompt('what is its value?')
这些在语义上也是等价的。 第二种方法的优点是属性的名称以字符串形式提供,这意味着它可以在运行时计算。 但是,使用此方法可以防止应用某些JavaScript引擎和minifier优化。 它还可用于设置和获取名称为保留字的属性:
obj.for = 'Simon'; // Syntax error, because 'for' is a reserved word
obj['for'] = 'Simon'; // works fine
从ECMAScript 5开始,保留字可以用作“缓存中”的对象属性名称。 这意味着在定义对象文字时,它们不需要包含在引号中。 参见ES5 Spec。
有关对象和原型的更多信息,请参阅Object.prototype。 有关对象原型和对象原型链的说明,请参阅继承和原型链。
从ECMAScript 2015开始,对象键可以在创建时使用括号表示法由变量定义。
{[phoneType]:12345}
是可能的,而不仅仅是var userPhone = {}; userPhone[phoneType] = 12345
。
数组
JavaScript中的数组实际上是一种特殊类型的对象。 它们非常类似于常规对象(数值属性当然只能使用[]
语法访问),但它们有一个名为“length
”的魔术属性。 这总是比数组中的最高索引多一个。
创建数组的一种方法如下:
var a = new Array();
a[0] = 'dog';
a[1] = 'cat';
a[2] = 'hen';
a.length; // 3
更方便的表示法是使用数组文字:
var a = ['dog', 'cat', 'hen'];
a.length; // 3
请注意,array.length
不一定是数组中的项目数。 考虑以下:
var a = ['dog', 'cat', 'hen'];
a[100] = 'fox';
a.length; // 101
请记住 - 数组的长度比最高索引多一个。
如果查询不存在的数组索引,则返回值为undefined
:
typeof a[90]; // undefined
如果考虑上面的[]
和length
,可以使用以下for
循环遍历数组:
for (var i = 0; i < a.length; i++) {// Do something with a[i]
}
ES2015为可迭代对象(如数组)引入了更简洁的循环:
for (const currentValue of a) {// Do something with currentValue
}
您也可以使用for … in循环遍历数组,但是这不会迭代数组元素,而是迭代数组索引。 此外,如果有人向Array.prototype
添加了新属性,那么它们也会被这样的循环迭代。 因此,不建议将此循环类型用于数组。
迭代使用ECMAScript 5添加的数组的另一种方法是forEach():
['dog', 'cat', 'hen'].forEach(function(currentValue, index, array) {// Do something with currentValue or array[index]
});
如果要将项附加到数组,只需这样做:
a.push(item);
数组有许多方法。 另请参见数组方法的完整文档。
方法名 | 描述 |
---|---|
a.toString()
|
返回一个字符串,其中每个元素的toString() 用逗号分隔。
|
a.toLocaleString()
|
返回一个字符串,其中每个元素的toLocaleString() 用逗号分隔。
|
a.concat(item1[, item2[, ...[, itemN]]])
|
返回一个新数组,其中添加了项目。 |
a.join(sep)
|
将数组转换为字符串 - 使用sep 参数分隔的值
|
a.pop()
|
删除并返回最后一项。 |
a.push(item1, ..., itemN)
|
将项追加到数组的末尾。 |
a.reverse()
|
反转数组。 |
a.shift()
|
删除并返回第一个项目。 |
a.slice(start[, end])
|
返回一个子数组。 |
a.sort([cmpfn])
|
采用可选的比较功能。 |
a.splice(start, delcount[, item1[, ...[, itemN]]])
|
允许您通过删除节并将其替换为更多项来修改数组。 |
a.unshift(item1[, item2[, ...[, itemN]]])
|
将项目添加到数组的开头。 |
函数
与对象一起,函数是理解JavaScript的核心组件。 最基本的功能不能更简单了:
function add(x, y) {var total = x + y;return total;
}
这表示了一个基本函数。 JavaScript函数可以使用0个或多个命名参数。 函数体可以包含任意数量的语句,并且可以声明它自己的变量,这些变量是该函数的本地变量。 return
语句可以用来随时返回一个值,结束该函数。 如果没有使用return语句(或没有值的空返回),则JavaScript返回undefined
。
命名参数更像是指南而不是其它东西。 您可以在不传递预期参数的情况下调用函数,在这种情况下,它们将设置为undefined
。
add(); // NaN
// You can't perform addition on undefined
您还可以传入比函数所期望的更多的参数:
add(2, 3, 4); // 5
// added the first two; 4 was ignored
这可能看起来有些愚蠢,但是函数可以在其体内访问一个名为arguments
的附加变量,这是一个类似于数组的对象,包含传递给函数的所有值。 让我们重新编写add函数以获取任意数量的值:
function add() {var sum = 0;for (var i = 0, j = arguments.length; i < j; i++) {sum += arguments[i];}return sum;
}add(2, 3, 4, 5); // 14
这真的没有比写2 + 3 + 4 + 5更有用了。 让我们创建一个平均函数:
function avg() {var sum = 0;for (var i = 0, j = arguments.length; i < j; i++) {sum += arguments[i];}return sum / arguments.length;
}avg(2, 3, 4, 5); // 3.5
这非常有用,但看起来确实有些冗长。 为了减少这个代码,我们可以考虑通过剩余参数语法替换arguments数组的使用。 通过这种方式,我们可以将任意数量的参数传递给函数,同时保持代码最小化。 剩余参数运算符在函数参数列表中使用,格式为:…variable,它将在该变量中包含调用该函数的未捕获参数的完整列表。 我们还将使用for…of循环替换for循环以返回变量中的值。
function avg(...args) {var sum = 0;for (let value of args) {sum += value;}return sum / args.length;
}avg(2, 3, 4, 5); // 3.5
在上面的代码中,变量args包含传递给函数的所有值。
重要的是要注意,无论在函数声明中放置剩余参数运算符,它都会在声明之后存储所有参数,但不会在之前存储。 例如*function avg(firstValue,… args)*将存储传递给firstValue变量中的函数的第一个值以及args中的其余参数。 这是另一个有用的语言功能,但它确实引发了一个新问题。
avg()
函数采用以逗号分隔的参数列表 - 但如果要查找数组的平均值,该怎么办? 您可以按如下方式重写该函数:function avgArray(arr) {var sum = 0;for (var i = 0, j = arr.length; i < j; i++) {sum += arr[i];}return sum / arr.length; }avgArray([2, 3, 4, 5]); // 3.5
但是能够重用我们已经创建的函数会更好。 幸运的是,JavaScript允许您使用任何函数对象的apply()
方法调用具有任意参数数组的函数。
avg.apply(null, [2, 3, 4, 5]); // 3.5
apply()
的第二个参数是用作参数的数组; 第一个将在稍后讨论。 这强调了函数也是对象的事实。
您可以使用函数调用中的spread运算符来获得相同的结果。
例如:
avg(...numbers)
JavaScript允许您创建匿名函数。
var avg = function() {var sum = 0;for (var i = 0, j = arguments.length; i < j; i++) {sum += arguments[i];}return sum / arguments.length;
};
这在语义上等同于function avg()
形式。 它非常强大,因为它允许您在通常放置表达式的任何位置放置完整的函数定义。 这可以实现各种巧妙的技巧。 这是一种“隐藏”一些局部变量的方法 - 比如C中的块范围:
var a = 1;
var b = 2;(function() {var b = 3;a += b;
})();a; // 4
b; // 2
JavaScript允许您递归调用函数。 这对于处理树结构特别有用,例如浏览器DOM中的树结构。
function countChars(elm) {if (elm.nodeType == 3) { // TEXT_NODEreturn elm.nodeValue.length;}var count = 0;for (var i = 0, child; child = elm.childNodes[i]; i++) {count += countChars(child);}return count;
}
这凸显了匿名函数的潜在问题:如果没有名称,如何递归调用它们? JavaScript允许您为此命名函数表达式。 您可以使用命名IIFE(立即调用函数表达式),如下所示:
var charsInBody = (function counter(elm) {if (elm.nodeType == 3) { // TEXT_NODEreturn elm.nodeValue.length;}var count = 0;for (var i = 0, child; child = elm.childNodes[i]; i++) {count += counter(child);}return count;
})(document.body);
如上所述提供给函数表达式的名称仅可用于函数自己的作用域。 这允许引擎进行更多优化,并产生更易读的代码。 该名称还显示在调试器和一些堆栈跟踪中,这可以节省您在调试时的时间。
请注意,JavaScript函数本身就是对象 - 就像JavaScript中的其他所有东西一样 - 您可以像我们之前在“对象”部分中看到的那样添加或更改它们的属性。
自定义对象
有关JavaScript中面向对象编程的更详细讨论,请参阅面向对象的JavaScript简介。
在经典的面向对象编程中,对象是对该数据进行操作的数据和方法的集合。 JavaScript是一种基于原型的语言,它不包含任何类语句,正如您在C++或Java中所发现的那样(对于习惯于使用类语句的语言的程序员来说,这有时会令人困惑)。 相反,JavaScript使用函数作为类。 让我们考虑一个具有名字和姓氏字段的人物对象。 可以通过两种方式显示名称:“名 姓”或“姓, 名”。 使用我们之前讨论过的函数和对象,我们可以显示如下数据:
function makePerson(first, last) {return {first: first,last: last};
}
function personFullName(person) {return person.first + ' ' + person.last;
}
function personFullNameReversed(person) {return person.last + ', ' + person.first;
}var s = makePerson('Simon', 'Willison');
personFullName(s); // "Simon Willison"
personFullNameReversed(s); // "Willison, Simon"
这有效,但它非常难看。 您最终会在全局命名空间中使用许多函数。 我们真正需要的是一种将函数附加到对象的方法。 由于函数是对象,因此很容易:
function makePerson(first, last) {return {first: first,last: last,fullName: function() {return this.first + ' ' + this.last;},fullNameReversed: function() {return this.last + ', ' + this.first;}};
}var s = makePerson('Simon', 'Willison');
s.fullName(); // "Simon Willison"
s.fullNameReversed(); // "Willison, Simon"
注意this
关键字。 在函数内部使用,this
指的是当前对象。 实际意味着什么是通过调用该函数的方式来指定的。 如果您在对象上使用点表示法或括号表示法调用它,则该对象将成为this
。 如果点符号未用于调用,this
则表示全局对象。
请注意,this
很容易引起错误。 例如:
var s = makePerson('Simon', 'Willison');
var fullName = s.fullName;
fullName(); // undefined undefined
当我们单独调用fullName()
而不使用s.fullName()
时,this
将绑定到全局对象。 由于没有名为first
或last
的全局变量,因此每个变量都是undefined
。
我们可以利用this
关键字来改进makePerson
函数:
function Person(first, last) {this.first = first;this.last = last;this.fullName = function() {return this.first + ' ' + this.last;};this.fullNameReversed = function() {return this.last + ', ' + this.first;};
}
var s = new Person('Simon', 'Willison');
我们引入了另一个关键字:new
。 new
与this
密切相关。 它创建一个全新的空对象,然后调用指定的函数,并将this
设置为该新对象。 请注意,尽管用this
指定的函数不会返回值,而只是修改this
对象。 但是new
会将this
对象返回给调用者。 设计为由new
调用的函数称为构造函数。 通常的做法是将这些功能大写,以提醒用new
来调用它们。
改进的函数仍然具有与单独调用fullName()
相同的缺陷。
我们的person对象越来越好,但仍然有一些丑陋的边缘。 每次我们创建一个person对象时,我们都会在其中创建两个全新的函数对象 - 如果这个代码被共享会不会更好?
function personFullName() {return this.first + ' ' + this.last;
}
function personFullNameReversed() {return this.last + ', ' + this.first;
}
function Person(first, last) {this.first = first;this.last = last;this.fullName = personFullName;this.fullNameReversed = personFullNameReversed;
}
那样更好:我们只创建一次方法函数,并在构造函数中为它们分配引用。 我们可以做得更好吗? 答案是肯定的:
function Person(first, last) {this.first = first;this.last = last;
}
Person.prototype.fullName = function() {return this.first + ' ' + this.last;
};
Person.prototype.fullNameReversed = function() {return this.last + ', ' + this.first;
};
Person.prototype
是Person
的所有实例共享的对象。 它构成了查找链的一部分(具有特殊名称,“原型链”):只要您尝试访问未设置的Person
属性,JavaScript就会检查Person.prototype
以查看该属性是否存在。 因此,分配给Person.prototype
的任何内容都可通过this
对象用于该构造函数的所有实例。
这是一个非常强大的工具。 JavaScript允许您在程序中随时修改某些原型,这意味着您可以在运行时向现有对象添加额外的方法:
var s = new Person('Simon', 'Willison');
s.firstNameCaps(); // TypeError on line 1: s.firstNameCaps is not a functionPerson.prototype.firstNameCaps = function() {return this.first.toUpperCase();
};
s.firstNameCaps(); // "SIMON"
有趣的是,您还可以将内容添加到内置JavaScript对象的原型中。 让我们为String
添加一个方法,以反向返回该字符串:
var s = 'Simon';
s.reversed(); // TypeError on line 1: s.reversed is not a functionString.prototype.reversed = function() {var r = '';for (var i = this.length - 1; i >= 0; i--) {r += this[i];}return r;
};s.reversed(); // nomiS
我们的新方法甚至适用于字符串文字!
'This can now be reversed'.reversed(); // desrever eb won nac sihT
如前所述,原型构成了链条的一部分。 该链的根是Object.prototype
,其方法包括toString()
- 当您尝试将对象表示为字符串时,将调用此方法。 这对调试Person
对象很有用:
var s = new Person('Simon', 'Willison');
s.toString(); // [object Object]Person.prototype.toString = function() {return '<Person: ' + this.fullName() + '>';
}s.toString(); // "<Person: Simon Willison>"
还记得avg.apply()
有一个null的第一个参数吗? 我们现在可以重新审视。 apply()
的第一个参数是应该被视为’this
'的对象。 例如,这是一个简单的new
实现:
function trivialNew(constructor, ...args) {var o = {}; // Create an objectconstructor.apply(o, args);return o;
}
这不是new
的精确复制品,因为它没有设置原型链(很难说明)。 这不是你经常使用的东西,但知道它是有用的。 在这个片段中,... args
(包括省略号)被称为“rest arguments” - 顾名思义,它包含其余的参数。
var bill = trivialNew(Person, 'William', 'Orange');
因此几乎相当于
var bill = new Person('William', 'Orange');
apply()
有一个名为call
的姐妹函数,它再次允许你设置this
,但是接受扩展参数列表而不是数组。
function lastNameCaps() {return this.last.toUpperCase();
}
var s = new Person('Simon', 'Willison');
lastNameCaps.call(s);
// Is the same as:
s.lastNameCaps = lastNameCaps;
s.lastNameCaps(); // WILLISON
内部函数
其他函数允许使用JavaScript函数声明。 我们之前见过这个,有一个早期的makePerson()
函数。 JavaScript中嵌套函数的一个重要细节是它们可以在父函数的作用域中访问变量:
function parentFunc() {var a = 1;function nestedFunc() {var b = 4; // parentFunc can't use thisreturn a + b; }return nestedFunc(); // 5
}
这在编写更易维护的代码方面提供了很多实用性。 如果被调用函数依赖于一个或两个对代码的任何其他部分无用的其他函数,则可以将这些函数函数嵌套在其中。 这样可以减少全局范围内的函数数量,这总是一件好事。
这也是全球变量诱惑的一个很好的对手。 在编写复杂代码时,通常很有可能使用全局变量在多个函数之间共享值 - 这会导致代码难以维护。 嵌套函数可以在其父级中共享变量,因此您可以使用该机制在有意义的情况下将函数耦合在一起,而不会污染您的全局命名空间 - 如果您愿意,可以使用“local globals”。 应该谨慎使用这种技术,但这是一种有用的能力。
闭包
这导致我们使用JavaScript提供的最强大的抽象之一 - 但也是最容易混淆的。 这是做什么的?
function makeAdder(a) {return function(b) {return a + b;};
}
var add5 = makeAdder(5);
var add20 = makeAdder(20);
add5(6); // ?
add20(7); // ?
makeAdder()
函数的名称可以无视掉:它创建新的’adder’函数,当用一个参数调用时,每个函数都将它添加到用它创建的参数中。
这里发生的事情与之前的内部函数几乎完全相同:在另一个函数内定义的函数可以访问外部函数的变量。 这里唯一的区别是外部函数已经返回,因此常识似乎要求它的局部变量不再存在。 但它们仍然存在 - 否则,加法器功能将无法工作。 更重要的是,makeAdder()
的局部变量有两个不同的“副本” - 一个是a是5而另一个是a是20.所以该函数调用的结果如下:
add5(6); // returns 11
add20(7); // returns 27
这是实际发生的事情。 每当JavaScript执行一个函数时,就会创建一个’scope’对象来保存在该函数中创建的局部变量。 它使用作为函数参数传入的任何变量进行初始化。 这类似于所有全局变量和函数所存在的全局对象,但有几个重要的区别:首先,每次函数开始执行时都会创建一个全新的范围对象,其次,与全局对象(例如this
以及浏览器中的window
)不同,不能在JavaScript代码中直接访问这些范围对象。 例如,没有用于迭代当前范围对象的属性的机制。
因此,当调用makeAdder()
时,将使用一个属性创建范围对象:a
,这是传递给makeAdder()
函数的参数。 makeAdder()
然后返回一个新创建的函数。 通常,JavaScript的垃圾收集器会清理此时为makeAdder()
创建的范围对象,但返回的函数会维护一个返回该范围对象的引用。 因此,只要makeAdder()
返回的函数对象还有引用,就不会对scope对象进行垃圾回收。
范围对象形成一个称为范围链的链,类似于JavaScript的对象系统使用的原型链。
闭包是函数和创建它的范围对象的组合。 闭包可以让你保存状态 - 因此,它们通常可以用来代替对象。 你可以找到几个有关闭包的优秀介绍。
重新介绍JavaScript(JS教程)相关推荐
- socket.js教程
好的,我来为你介绍 socket.js 教程. Socket.js 是一个用于建立 WebSocket 连接的 JavaScript 库,可以让你在浏览器和服务器之间进行双向通信. 首先,你需要安装 ...
- JavaScript(JS) date.getDay()
Date对象是JavaScript语言内建的数据类型.使用新的Date()创建日期对象.本文主要介绍JavaScript(JS) date.getDay() 方法. 原文地址:JavaScript(J ...
- JavaScript(JS) string.italics( )
String对象允许你处理一系列字符;它用许多辅助方法包装Javascript的字符串原始数据类型.当JavaScript在字符串原语和字符串对象之间自动转换时,可以在字符串原语上调用string对象 ...
- JavaScript(JS) Number.NaN
Number对象表示数字日期,可以是整数也可以是浮点数.通常,不需要担心Number对象,因为浏览器会自动将Number字面量转换为Number类的实例.本文主要介绍JavaScript(JS) Nu ...
- JavaScript(JS) Math.E
Math对象为数学常量和函数提供属性和方法.与其他全局对象不同,Math不是构造函数.Math的所有属性和方法都是静态的,可以通过将Math作为对象来调用,而无需创建它.本文主要介绍JavaScrip ...
- JavaScript(JS) array.splice(index, howMany, [element1][, ..., elementN])
Array对象允许在一个变量中存储多个值.它存储相同类型元素的固定大小的顺序集合.数组用于存储数据集合,但将数组看作同一类型变量的集合通常更有用.本文主要介绍JavaScript(JS) array. ...
- JavaScript(JS) 清空删除数组元素的多种方法及示例代码
简介: 本文主要介绍JavaScript(JS)中,清空删除数组中的所有元素的多种方法,以及相关的示例代码. 1.使用[]得到一个新数组进行清空 例如, var arr1 = ['a','b','c' ...
- JavaScript(JS) 面向对象(封装、继承、多态)
面向对象是把事物给对象化,包括其属性和行为.面向对象编程更贴近实际生活的思想.可以简单的理解面向对象的底层还是面向过程,面向过程抽象成类,然后封装,方便使用就是面向对象.本文主要介绍JavaScrip ...
- JavaScript(JS) string.charCodeAt(index)
String对象允许你处理一系列字符;它用许多辅助方法包装Javascript的字符串原始数据类型.当JavaScript在字符串原语和字符串对象之间自动转换时,可以在字符串原语上调用string对象 ...
- 视频教程-2020全新Javascript基础面试视频前端js教程-JavaScript
2020全新Javascript基础面试视频前端js教程 7年的开发架构经验,曾就职于国内一线互联网公司,开发工程师,现在是某创业公司技术负责人, 擅长语言有node/java/python,专注于服 ...
最新文章
- delphi公共函数 UMyPubFuncFroc--版权所有 (C) 2008 勇者工作室
- Skyline 扩展模块简介
- FPGA之道(44)HDL中的隐患写法
- Eclipse: “Update SVN cache” hangs and locks up
- Protocol Buffer C++应用实例
- EOS 共识机制 (5)超级节点投票
- supervisor使用指南
- 【BZOJ2286】消耗战(虚树,动态规划)
- 跟着大彬读源码 - Redis 1 - 启动服务,程序都干了什么?
- vs code react-native 安卓调试_实战|C++在vscode上的调试配置
- JSADS-日志对象MyLogger
- linux 解压war到root_unzip命令解压war包方法
- APP定制开发之前,这6条铁律要牢记
- 基于WebGIS的电子政务应用(基于J2EE的MVC架构)
- 开发中常用的几种 Content-Type以及图片上传前后端分离开发处理方式
- 观察者模式在游戏开发中的应用
- 如何在 XMind 中输入数学方程?LaTeX 简易入门
- 黑马教程python入门之基础笔记day1/2
- RTL8723BU移植
- 触摸屏 服务器系统,IP网络触摸屏服务器 SK1606