《JavaScript高级程序设计》(1)

第一章(什么是JavaScript)

js的组成部分

  1. ECMAScript:由ECMA-262定义并提供核心功能。
  2. 文档对象模型(DOM):提供与网页内容交互的方法和接口。
  3. 浏览器对象模型(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("&lt;/script&gt;");
  }
</script>

注:

  1. 使用了src属性的<script>元素不应该再在<script>和</script>标签中再包含其他JavaScript代码。如果两者都提供的话,则浏览器只会下载并执行脚本文件,从而忽略行内代码。
  2. 第二个<script>元素的代码必须在第一个<script>元素的代码解释完毕才能开始解释,第三个则必须等第二个解释完,以此类推。
  3. 导致页面渲染的明显延迟,在此期间浏览器窗口完全空白。 现代Web应用程序通常将所有JavaScript引用放在<body>元素中的页面内容后面
  4. 在<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>元素中的任何内容都不会被渲染。

第三章(语言基础)

变量

标识符

  1. 第一个字符必须是一个字母、下划线(_)或美元符号($);
  2. 按照惯例,ECMAScript标识符使用驼峰大小写形式,即第一个单词的首字母小写,后面每个单词的首字母大写
  3. 最佳实践是始终在控制语句中使用代码块,即使要执行的只有一条语句

var、const和let。其中,var在ECMAScript的所有版本中都可以使用,而const和let只能在ECMAScript 6及更晚的版本中使用。

var 注:这个声明现在基本不用了,了解即可。

  1. 作用域

使用var在一个函数内部定义一个变量,就意味着该变量将在函数退出时被销毁:


function test() {
  var message = "hi"; // 局部变量
}
test();
console.log(message); // 出错!

在函数内定义变量时省略var操作符,可以创建一个全局变量:

不推荐这样写

function test() {
  message="hi";     //全局变量
}
test();
console.log(message); // "hi"
  1. 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;
  • 两个命名参数不能拥有同一个名称。
    如果违反上述规则,则会导致语法错误,代码也不会执行。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值