第一章(什么是JavaScript)
js的组成部分:
- ECMAScript:由ECMA-262定义并提供核心功能。
- 文档对象模型(DOM):提供与网页内容交互的方法和接口。
- 浏览器对象模型(BOM):提供与浏览器交互的方法和接口。
第二章 (HTML中的JavaScript)
概述:本章了解外源js的引用规则
<script>标签
在<script>元素中的代码被计算完成之前,页面的其余内容不会被加载,也不会被显示。
在使用行内JavaScript代码时,要注意代码中不能出现字符串</script>。比如,下面的代码会导致浏览器报错:
<script>
function sayScript() {
console.log("</script>");
}
</script>
解决方案:转义字符
<script>
function sayScript() {
// 方法1:拆分字符串拼接
console.log("</sc" + "ript>");
// 方法2:使用转义字符(HTML实体转义,在JS中输出时有效)
console.log("</script>");
}
</script>
注:
- 使用了src属性的<script>元素不应该再在<script>和</script>标签中再包含其他JavaScript代码。如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。
- 第二个<script>元素的代码必须在第一个<script>元素的代码解释完毕才能开始解释,第三个则必须等第二个解释完,以此类推。
- 导致页面渲染的明显延迟,在此期间浏览器窗口完全空白。 现代Web应用程序通常将所有JavaScript引用放在<body>元素中的页面内容后面
- 在<script>元素上设置defer属性,相当于告诉浏览器立即下载,但延迟执行。考虑到这一点,还是把要推迟执行的脚本放在页面底部比较好。
动态加载脚本
只要创建一个script元素并将其添加到DOM即可。
let script = document.createElement('script');
script.src = 'gibberish.js';
document.head.appendChild(script);
行内代码与外部文件
- 可维护性
- JavaScript代码如果分散到很多HTML页面,会导致维护困难。而用一个目录保存所有JavaScript文件,则更容易维护,这样开发者就可以独立于使用它们的HTML页面来编辑代码。
- 缓存
- 浏览器会根据特定的设置缓存所有外部链接的JavaScript文件,这意味着如果两个页面都用到同一个文件,则该文件只需下载一次。这最终意味着页面加载更快。
- 适应未来
- 通过把JavaScript放到外部文件中,就不必考虑用XHTML或前面提到的注释黑科技。包含外部JavaScript文件的语法在HTML和XHTML中是一样的。
总结
- 要包含外部JavaScript文件,必须将src属性设置为要包含文件的URL。文件可以跟网页在同一台服务器上,也可以位于完全不同的域。
- 所有<script>元素会依照它们在网页中出现的次序被解释。在不使用defer和async属性的情况下,包含在<script>元素中的代码必须严格按次序解释。
- 对不推迟执行的脚本,浏览器必须解释完位于<script>元素中的代码,然后才能继续渲染页面的剩余部分。为此,通常应该把<script>元素放到页面末尾,介于主内容之后及</body>标签之前。
- 可以使用defer属性把脚本推迟到文档渲染完毕后再执行。推迟的脚本原则上按照它们被列出的次序执行。
- 可以使用async属性表示脚本不需要等待其他脚本,同时也不阻塞文档渲染,即异步加载。异步脚本不能保证按照它们在页面中出现的次序执行。
- 通过使用<noscript>元素,可以指定在浏览器不支持脚本时显示的内容。如果浏览器支持并启用脚本,则<noscript>元素中的任何内容都不会被渲染。
第三章(语言基础)
变量
标识符
- 第一个字符必须是一个字母、下划线(_)或美元符号($);
- 按照惯例,ECMAScript标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写
- 最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句
var、const和let。其中,var在ECMAScript的所有版本中都可以使用,而const和let只能在ECMAScript 6及更晚的版本中使用。
var 注:这个声明现在基本不用了,了解即可。
- 作用域
使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:
function test() {
var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!
在函数内定义变量时省略var操作符,可以创建一个全局变量:
不推荐这样写
function test() {
message="hi"; //全局变量
}
test();
console.log(message); // "hi"
- var声明提升
function foo() {
console.log(age);
var age = 26;
}
foo(); // undefined
之所以不会报错,是因为ECMAScript运行时把它看成等价于如下代码:
function foo() {
var age;
console.log(age);
age = 26;
}
foo(); // undefined
这就是所谓的“提升”(hoist),也就是把所有变量声明都拉到函数作用域的顶部。此外,反复多次使用var声明同一个变量也没有问题:
function foo() {
var age = 16;
var age = 26;
var age = 36;
console.log(age);
}
foo(); // 36
let
let声明的范围是块作用域,而var声明的范围是函数作用域。
if (true) {
var name = 'Matt';
console.log(name); // Matt
}
console.log(name); // Matt
if (true) {
let age = 26;
console.log(age); // 26
}
console.log(age); // ReferenceError: age没有定义
let也不允许同一个块作用域中出现冗余声明。这样会导致报错。
这两个关键字声明的并不是不同类型的变量,它们只是指出变量在相关作用域如何存在。
var name;
let name; // SyntaxError
let age;
var age; // SyntaxError
let与var的另一个重要的区别,就是let声明的变量不会在作用域中被提升。
// name会被提升
console.log(name); // undefined
var name = 'Matt';
// age不会被提升
console.log(age); // ReferenceError:age没有定义
let age = 26;
2.全局声明
与var关键字不同,使用let在全局作用域中声明的变量不会成为window对象的属性(var声明的变量则会)。
var name = 'Matt';
console.log(window.name); // 'Matt'
let age = 26;
console.log(window.age); // undefined
3.for循环中的let声明
在let出现之前,for循环定义的迭代变量会渗透到循环体外部:
for (var i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // 5
改成使用let之后,这个问题就消失了,因为迭代变量的作用域仅限于for循环块内部:
for (let i = 0; i < 5; ++i) {
// 循环逻辑
}
console.log(i); // ReferenceError: i没有定义
在使用var的时候,最常见的问题就是对迭代变量的奇特声明和修改:
for (var i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 你可能以为会输出0、1、2、3、4
// 实际上会输出5、5、5、5、5
之所以会这样,是因为在退出循环时,迭代变量保存的是导致循环退出的值:5。在之后执行超时逻辑时,所有的i都是同一个变量,因而输出的都是同一个最终值。
而在使用let声明迭代变量时,JavaScript引擎在后台会为每个迭代循环声明一个新的迭代变量。每个setTimeout引用的都是不同的变量实例,所以console.log输出的是我们期望的值,也就是循环执行过程中每个迭代变量的值。
for (let i = 0; i < 5; ++i) {
setTimeout(() => console.log(i), 0)
}
// 会输出0、1、2、3、4
const
const的行为与let基本相同,唯一一个重要的区别是用它声明变量时必须同时初始化变量,且尝试修改const声明的变量会导致运行时错误。
const声明的限制只适用于它指向的变量的引用。换句话说,如果const变量引用的是一个对象,那么修改这个对象内部的属性并不违反const的限制。
最佳实践
1.不使用var
有了let和const,大多数开发者会发现自己不再需要var了。限制自己只使用let和const有助于提升代码质量,因为变量有了明确的作用域、声明位置,以及不变的值。
2.const优先,let次之
使用const声明可以让浏览器运行时强制保持变量不变,也可以让静态代码分析工具提前发现不合法的赋值操作。因此,很多开发者认为应该优先使用const来声明变量,只在提前知道未来会有修改时,再使用let。这样可以让开发者更有信心地推断某些变量的值永远不会变,同时也能迅速发现因意外赋值导致的非预期行为。
数据类型
对一个值使用typeof操作符会返回下列字符串之一:
- "undefined"表示值未定义;
- "boolean"表示值为布尔值;
- "string"表示值为字符串;
- "number"表示值为数值;
- "object"表示值为对象(而不是函数)或null;
- "function"表示值为函数;
- "symbol"表示值为符号。
下面是使用typeof操作符的例子:
let message = "some string";
console.log(typeof message); // "string"
console.log(typeof(message)); // "string"
console.log(typeof 95); // "number"
调用typeof null返回的是"object"。这是因为特殊值null被认为是一个对空对象的引用。
注意 严格来讲,函数在ECMAScript中被认为是对象,并不代表一种数据类型。可是,函数也有自己特殊的属性。为此,就有必要通过typeof操作符来区分函数和其他对象。
Undefined
Undefined类型只有一个值,就是特殊值undefined。当使用var或let声明了变量但没有初始化时,就相当于给变量赋予了undefined值:
let message;
console.log(message == undefined); // true
为什么要存在这个类型?
增加这个特殊值的目的就是为了正式明确空对象指针(null)和未初始化变量的区别。
在对未初始化的变量调用typeof时,返回的结果是"undefined",但对未声明的变量调用它时,返回的结果还是"undefined"
比如下面的例子:
let message; // 这个变量被声明了,只是值为undefined
// 确保没有声明过这个变量
// let age
console.log(typeofmessage);//"undefined"
console.log(typeofage); //"undefined"
Underfined 是一个假值
Null
null值表示一个空对象指针,这也是给typeof传一个null会返回"object"的原因:
let car = null;
console.log(typeof car); // "object"
在定义将来要保存对象值的变量时,建议使用null来初始化,不要使用其他值。
undefined值是由null值派生而来的,因此ECMA-262将它们定义为表面上相等,如下面的例子所示:
console.log(null == undefined); // true
Boolean
Boolean()转型函数可以在任意类型的数据上调用,而且始终返回一个布尔值。
let message = "Hello world! ";
let messageAsBoolean = Boolean(message);
像if等流控制语句会自动执行其他类型值到布尔值的转换,例如:
let message = "Hello world! ";
if (message) {
console.log("Value is true");
}
布尔转换和其他语言类似,没有需要额外记忆的部分
Number类型
因为存储浮点值使用的内存空间是存储整数值的两倍,所以ECMAScript总是想方设法把值转换为整数。
let floatNum1 = 1.; // 小数点后面没有数字,当成整数1 处理
let floatNum2 = 10.0; // 小数点后面是零,当成整数10 处理
let floatNum = 3.125e7; // 等于31250000
例如0.00000000000000003。这个数值用科学记数法可以表示为3e-17
浮点值的精确度最高可达17位小数,但在算术计算中远不如整数精确。例如,0.1加0.2得到的不是0.3,而是0.30000000000000004。由于这种微小的舍入错误,导致很难测试特定的浮点值。比如下面的例子:
if (a + b == 0.3) { // 别这么干!
console.log("You got 0.3.");
}
无法表示的数
- 最小值
Number.MIN_VALUE,5e-324 - 最大值
Number.MAX_VALUE,1.797693134862315 7e+308 - 如果超出,数值会被自动转换成一个特殊的
Infinity(无穷),-Infinity(负无穷大)
要确定一个值是不是有限大(即介于JavaScript能表示的最小值和最大值之间),可以使用isFinite()函数,如下所示:
let result = Number.MAX_VALUE + Number.MAX_VALUE;
console.log(isFinite(result)); // false
NAN
有一个特殊的数值叫NaN,意思是“不是数值”(Not a Number),用于表示本来要返回数值的操作失败了(而不是抛出错误)。
console.log(0/0); // NaN
console.log(-0/+0); // NaN
如果分子是非0值,分母是有符号0或无符号0,则会返回Infinity或-Infinity:
console.log(5/0); // Infinity
console.log(5/-0); // -Infinity
首先,任何涉及NaN的操作始终返回NaN(如NaN/10),在连续多步计算时这可能是个问题。其次,NaN不等于包括NaN在内的任何值。例如,下面的比较操作会返回false:
console.log(NaN == NaN); // false
为此,ECMAScript提供了isNaN()函数。该函数接收一个参数,可以是任意数据类型,然后判断这个参数是否“不是数值”。
console.log(isNaN(NaN)); // true
console.log(isNaN(10)); // false,10 是数值
console.log(isNaN("10")); // false,可以转换为数值10
console.log(isNaN("blue")); // true,不可以转换为数值
console.log(isNaN(true)); // false,可以转换为数值1
数值转换
- Number()
- parseInt()
- parseFloat()。
Number()是转型函数,可用于任何数据类型。后两个函数主要用于将字符串转换为数值。
Number()
- 布尔值,true转换为1,false转换为0。
- 数值,直接返回。
- null,返回0。
- undefined,返回NaN。
- 字符串
- 如果字符串包含数值字符,包括数值字符前面带加、减号的情况,则转换为一个十进制数值。因此,Number(“1”)返回1, Number(“123”)返回123, Number(“011”)返回11(忽略前面的零)。
- 如果字符串包含有效的浮点值格式如"1.1",则会转换为相应的浮点值(同样,忽略前面的零)。
- 如果字符串包含有效的十六进制格式如"0xf",则会转换为与该十六进制值对应的十进制整数值。
- 如果是空字符串(不包含字符),则返回0。
- 如果字符串包含除上述情况之外的其他字符,则返回NaN。
- 对象,调用valueOf()方法,并按照上述规则转换返回的值。如果转换结果是NaN,则调用toString()方法,再按照转换字符串的规则转换。
let num1 = Number("Hello world! "); // NaN
let num2 = Number(""); // 0
let num3 = Number("000011"); // 11
let num4 = Number(true); // 1
parseInt()函数更专注于字符串是否包含数值模式。字符串最前面的空格会被忽略,从第一个非空格字符开始转换。如果第一个字符不是数值字符、加号或减号,parseInt()立即返回NaN。这意味着空字符串也会返回NaN(这一点跟Number()不一样,它返回0)。如果第一个字符是数值字符、加号或减号,则继续依次检测每个字符,直到字符串末尾,或碰到非数值字符。比如,"1234blue"会被转换为1234,因为"blue"会被完全忽略。类似地,"22.5"会被转换为22,因为小数点不是有效的整数字符。
let num1 = parseInt("1234blue"); // 1234
let num2 = parseInt(""); // NaN
let num3 = parseInt("0xA"); // 10,解释为十六进制整数
let num4 = parseInt(22.5); // 22
let num5 = parseInt("70"); // 70,解释为十进制值
let num6 = parseInt("0xf"); // 15,解释为十六进制整数
因此parseInt()也接收第二个参数,用于指定底数(进制数)。
let num = parseInt("0xAF", 16); // 175
let num1 = parseInt("AF", 16); // 175
let num2 = parseInt("AF"); // NaN
parseFloat()函数的工作方式跟parseInt()函数类似
意味着第一次出现的小数点是有效的,但第二次出现的小数点就无效了,此时字符串的剩余字符都会被忽略。因此,"22.34.5"将转换成22.34。
如果字符串表示整数(没有小数点或者小数点后面只有一个零),则parseFloat()返回整数
let num1 = parseFloat("1234blue"); // 1234,按整数解析
let num2 = parseFloat("0xA"); // 0
let num3 = parseFloat("22.5"); // 22.5
let num4 = parseFloat("22.34.5"); // 22.34
let num5 = parseFloat("0908.5"); // 908.5
let num6 = parseFloat("3.125e7"); // 31250000
String
字符串可以使用双引号(")、单引号(')或反引号(`)标示
ECMAScript中的字符串是不可变的(immutable),意思是一旦创建,它们的值就不能变了。要修改某个变量中的字符串值,必须先销毁原始的字符串,然后将包含新值的另一个字符串保存到该变量
有两种方式把一个值转换为字符串。首先是使用几乎所有值都有的toString()方法。
let age = 11;
let ageAsString = age.toString(); // 字符串"11"
let found = true;
let foundAsString = found.toString(); // 字符串"true"
toString()方法可见于数值、布尔值、对象和字符串值。(没错,字符串值也有toString()方法,该方法只是简单地返回自身的一个副本。)null和undefined值没有toString()方法。
多数情况下,toString()不接收任何参数。不过,在对数值调用这个方法时,toString()可以接收一个底数参数,即以什么底数来输出数值的字符串表示(默认十进制)
let num = 10;
console.log(num.toString()); // "10"
console.log(num.toString(2)); // "1010"
console.log(num.toString(8)); // "12"
console.log(num.toString(10)); // "10"
console.log(num.toString(16)); // "a"
如果你不确定一个值是不是null或undefined,可以使用String()转型函数,它始终会返回表示相应类型值的字符串。String()函数遵循如下规则。
- 如果值有toString()方法,则调用该方法(不传参数)并返回结果。
- 如果值是null,返回"null"。
- 如果值是undefined,返回"undefined"。
let value1 = 10;
let value2 = true;
let value3 = null;
let value4;
console.log(String(value1)); // "10"
console.log(String(value2)); // "true"
console.log(String(value3)); // "null"
console.log(String(value4)); // "undefined"
注:用加号操作符给一个值加上一个空字符串""也可以将其转换为字符串
模板字面量
模板字面量:与使用单引号或双引号不同,模板字面量保留换行字符,可以跨行定义字符串:
let myMultiLineTemplateLiteral = `first line
second line`;
console.log(myMultiLineTemplateLiteral);
// first line
// second line
顾名思义,模板字面量在定义模板时特别有用,比如下面这个HTML模板:
let pageHTML = `
<div>
<a href="#">
<span>Jake</span>
</a>
</div>`;
模板字面量最常用的一个特性是支持字符串插值
字符串插值通过在${}中使用一个JavaScript表达式实现:
所有插入的值都会使用toString()强制转型为字符串,而且任何JavaScript表达式都可以用于插值。
模板字面量标签函数
不知道能干嘛
let a = 6;
let b = 9;
functionsimpleTag(strings, ...expressions){
console.log(strings);
for(const expression of expressions) {
console.log(expression);
}
return 'foobar';
}
let taggedResult = simpleTag`${ a } + ${ b } = ${ a + b }`;
// ["", " + ", " = ", ""]
// 6
// 9
// 15
console.log(taggedResult); // "foobar"
原始字符串
String.raw标签函数
console.log(`\\u00A9`); // ©
console.log(String.raw`\\u00A9`); // \\u00A9
标签函数,String.raw返回的是一个字符串数组
function printRaw(strings) {
console.log('Actual characters:');
for (const string of strings) {
console.log(string);
}
console.log('Escaped characters; ');
for (const rawString of strings.raw) {
console.log(rawString);
}
}
printRaw`\\u00A9${ 'and' }\\n`;
// Actual characters:
// ©
//(换行符)
// Escaped characters:
// \\u00A9
// \\n
Symbol类型
Symbol(符号)是ECMAScript 6新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
Object类型
- constructor:用于创建当前对象的函数
- hasOwnProperty(propertyName):用于判断当前对象实例(不是原型)上是否存在给定的属性。要检查的属性名必须是字符串(如o.hasOwnProperty(“name”))或符号。
- isPrototypeOf(object):用于判断当前对象是否为另一个对象的原型。(第8章将详细介绍原型。)
- propertyIsEnumerable(propertyName):用于判断给定的属性是否可以使用(本章稍后讨论的)for-in语句枚举。与hasOwnProperty()一样,属性名必须是字符串。
- toLocaleString():返回对象的字符串表示,该字符串反映对象所在的本地化执行环境。
- toString():返回对象的字符串表示。
- valueOf():返回对象对应的字符串、数值或布尔值表示。通常与toString()的返回值相同。
操作符
在应用给对象时,操作符通常会调用valueOf()和/或toString()方法来取得可以计算的值。
语句
for-in
for-in语句是一种严格的迭代语句,用于枚举对象中的非符号键属性
for (const propName in window) {
document.write(propName);
}
这里控制语句中的const也不是必需的。但为了确保这个局部变量不被修改,推荐使用const
for-of
for-of语句是一种严格的迭代语句,用于遍历可迭代对象的元素
for (const el of [2,4,6,8]) {
document.write(el);
}
for-of循环会按照可迭代对象的next()方法产生值的顺序迭代元素。关于可迭代对象,本书将在第7章详细介绍。
with
with语句的用途是将代码作用域设置为特定的对象
使用with语句的主要场景是针对一个对象反复操作,这时候将代码作用域设置为该对象能提供便利,如下面的例子所示:
let qs = location.search.substring(1);
let hostName = location.hostname;
let url = location.href;
上面代码中的每一行都用到了location对象。如果使用with语句,就可以少写一些代码:
with(location) {
let qs = search.substring(1);
let hostName = hostname;
let url = href;
}
由于with语句影响性能且难于调试其中的代码,通常不推荐在产品代码中使用with语句
函数
return语句也可以不带返回值。这时候,函数会立即停止执行并返回undefined。
最佳实践:函数要么返回值,要么不返回值。只在某个条件下返回值的函数会带来麻烦,尤其是调试时
严格模式对函数也有一些限制:
- 函数不能以eval或arguments作为名称;
- 函数的参数不能叫eval或arguments;
- 两个命名参数不能拥有同一个名称。
如果违反上述规则,则会导致语法错误,代码也不会执行。
564

被折叠的 条评论
为什么被折叠?



