JavaScript

1、什么是JavaScript

1.1、概述

JavaScript是世界上最流行的脚本语言,因为你在电脑、手机、平板上浏览的所有的网页,以及无数基于HTML5的手机App,交互逻辑都是由JavaScript驱动的。

简单地说,JavaScript是一种运行在浏览器中的解释型的编程语言。

在Web世界里,只有JavaScript能跨平台、跨浏览器驱动网页,与用户交互。

Flash背后的ActionScript曾经流行过一阵子,不过随着移动应用的兴起,没有人用Flash开发手机App,所以它目前已经边缘化了。相反,随着HTML5在PC和移动端越来越流行,JavaScript变得更加重要了。并且,新兴的Node.js把JavaScript引入到了服务器端,JavaScript已经变成了全能型选手。

JavaScript一度被认为是一种玩具编程语言,它有很多缺陷,所以不被大多数后端开发人员所重视。很多人认为,写JavaScript代码很简单,并且JavaScript只是为了在网页上添加一点交互和动画效果。但这是完全错误的理解。JavaScript确实很容易上手,但其精髓却不为大多数开发人员所熟知。编写高质量的JavaScript代码更是难上加难。

一个合格的开发人员应该精通JavaScript和其他编程语言。如果你已经掌握了其他编程语言,或者你还什么都不会,请立刻开始学习JavaScript,不要被Web时代所淘汰。

1.2、历史

在上个世纪的1995年,当时的网景公司正凭借其Navigator浏览器成为Web时代开启时最著名的第一代互联网公司。

由于网景公司希望能在静态HTML页面上添加一些动态效果,于是叫Brendan Eich这哥们在两周之内设计出了JavaScript语言。你没看错,这哥们只用了10天时间。

为什么起名叫JavaScript?原因是当时Java语言非常红火,所以网景公司希望借Java的名气来推广,但事实上JavaScript除了语法上有点像Java,其他部分基本上没啥关系。

1.3、ECMAScript

因为网景开发了JavaScript,一年后微软又模仿JavaScript开发了JScript,为了让JavaScript成为全球标准,几个公司联合ECMA(European Computer Manufacturers Association)组织定制了JavaScript语言的标准,被称为ECMAScript标准。

所以简单说来就是,ECMAScript是一种语言标准,而JavaScript是网景公司对ECMAScript标准的一种实现。

不过大多数时候,我们还是用JavaScript这个词。如果你遇到ECMAScript这个词,简单把它替换为JavaScript就行了。

2、快熟入门

2.1、引入JavaScript

JavaScript代码可以直接嵌在网页的任何地方,不过通常我们都把JavaScript代码放到 head 中:

1.内部标签

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>

    
    <script>
        alert('hello world');
    script>
head>
<body>


body>
html>

2.外部引入

第二种方法是把JavaScript代码放到一个单独的.js文件,然后在HTML中通过 引入这个文件:

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>
    
	
    <script src="js/zhr.js">script>   
head>
<body>
...
body>
html>

zhr.js文件

alert('hello world');

有些时候你会看到

<script type="text/javascript">
    ... 
script>

2.2、基本语法入门

JavaScript的语法和Java语言类似,每个语句以;结束,语句块用{...}

但是,JavaScript并不强制要求在每个语句的结尾加;,浏览器中负责执行JavaScript代码的引擎会自动在每个语句的结尾补上;

让JavaScript引擎自动加分号在某些情况下会改变程序的语义,导致运行结果与期望不一致。在本教程中,我们不会省略;,所有语句都会添加;

例如,下面的一行代码就是一个完整的赋值语句:

var x = 1;

下面的一行代码是一个字符串,但仍然可以视为一个完整的语句:

'Hello, world';

下面的一行代码包含两个语句,每个语句用; 表示语句结束:

var x = 1; var y = 2; // 不建议一行写多个语句!

语句块是一组语句的集合,例如,下面的代码先做了一个判断,如果判断成立,将执行 {...}中的所有语句:

if (2 > 1) {
    x = 1;
    y = 2; 
    z = 3; 
}

注意花括号{...}内的语句具有缩进,通常是4个空格。缩进不是JavaScript语法要求必须的,但缩进有助于我们理解代码的层次,所以编写代码时要遵守缩进规则。很多文本编辑器具有“自动缩进”的功能,可以帮助整理代码。

JavaScript本身对嵌套的层级没有限制,但是过多的嵌套无疑会大大增加看懂代码的难度。遇到这种情况,需要把部分代码抽出来,作为函数来调用,这样可以减少代码的复杂度。

另一种块注释是用 /*...*/把多行字符包裹起来,把一大“块”视为一个注释:

/* 从这里开始是块注释
仍然是注释 
仍然是注释 
注释结束 */

请注意,JavaScript严格区分大小写,如果弄错了大小写,程序将报错或者运行不正常。

DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Titletitle>

    <script>
        // 1.定义变量  var 变量名 = 变量值
        var score = 71;
        //alert(name)
        // 2.条件控制
        if(score>60 && score<70){
            alert("60~70")
        }else if(score>70 && score<80){
            alert("70~80")
        }else {
            alert("other")
        }
        //console.log(score) 在;浏览器的控制台打印变量 system.out.println()
    script>
head>
<body>
body>
html>

2.3、数据类型

计算机顾名思义就是可以做数学计算的机器,因此,计算机程序理所当然地可以处理各种数值。但是,计算机能处理的远不止数值,还可以处理文本、图形、音频、视频、网页等各种各样的数据,不同的数据,需要定义不同的数据类型。在JavaScript中定义了以下几种数据类型:

Number

JavaScript不区分整数和浮点数,统一用Number表示,以下都是合法的Number类型:

123; // 整数123
0.456; // 浮点数0.456 
1.2345e3; // 科学计数法表示1.2345x1000,等同于1234.5 
-99; // 负数 
NaN; // NaN表示Not a Number,当无法计算结果时用NaN表示 
Infinity; // Infinity表示无限大,当数值超过了JavaScript的Number所能表示的最大值时,就 表示为Infinity

Number可以直接做四则运算,规则和数学一致:

1 + 2; // 3
(1 + 2) * 5 / 2; // 7.5
2 / 0; // Infinity 
0 / 0; // NaN
10 % 3; // 1 
10.5 % 3; // 1.5

%是求余运算

字符串

字符串是以单引号’或双引号"括起来的任意文本,比如 'abc'"xyz" 等等。请注意,''""本身只是一种表示方式,不是字符串的一部分,因此,字符串'abc'只有abc这3个字符。

布尔值

布尔值和布尔代数的表示完全一致,一个布尔值只有truefalse两种值,要么是true ,要么是false ,可以直接用truefalse 表示布尔值,也可以通过布尔运算计算出来:

逻辑运算

&& 与运算,两个为真,结果才为真
|| 或运算,一个为真,结果就为真
! 非 

比较运算符

=
==  等于 (类型不一样,值一样,也会判断为true)
=== 绝对等于  (类型一样,值也一样,结果为true)

实际上,JavaScript允许对任意数据类型做比较:

false == 0; // true
false === 0; // false

由于JavaScript这个设计缺陷,不要使用== 比较,始终坚持使用===比较。

另一个例外是 NaN这个特殊的Number与所有其他值都不相等,包括它自己:

NaN === NaN; // false

唯一能判断NaN 的方法是通过isNaN()函数:

isNaN(NaN); // true

最后要注意浮点数的相等比较:

1 / 3 === (1 - 2 / 3); // false

这不是JavaScript的设计缺陷。浮点数在运算过程中会产生误差,因为计算机无法精确表示无限循环小数。要比较两个浮点数是否相等,只能计算它们之差的绝对值,看是否小于某个阈值:

Math.abs(1 / 3 - (1 - 2 / 3)) < 0.0000001; // true

null和undefined

null 表示一个“空”的值,它和 0 以及空字符串''不同, 0是一个数值,'' 表示长度为0的字符串,而 null表示“空”。

在其他语言中,也有类似JavaScript的null 的表示,例如Java也用null ,Swift用 nil ,Python用 None 表示。但是,在JavaScript中,还有一个和 null 类似的 undefined,它表示“未定义”。

JavaScript的设计者希望用 null表示一个空的值,而undefined 表示值未定义。事实证明,这并没有什么卵用,区分两者的意义不大。大多数情况下,我们都应该用 nullundefined仅仅在判断函数参数是否传递的情况下有用。

数组

数组是一组按顺序排列的集合,集合的每个值称为元素。JavaScript的数组可以包括任意数据类型。

//保证代码的可读性,尽量用【】
var arr = [1,2,3,4,null,'hello']
new Array(1,2,3,5,9);
var arr = [1, 2, 3.14, 'Hello', null, true]; 
arr[0]; // 返回索引为0的元素,即1
arr[5]; // 返回索引为5的元素,即true 
arr[6]; // 索引超出了范围,返回undefined

对象

JavaScript的对象是一组由键-值组成的无序集合,

var person = {
    name: 'Bob',
    age: 20,
    tags: ['js', 'web', 'mobile'], 
    city: 'Beijing', 
    hasCar: true,
    zipcode: null 
};

JavaScript对象的键都是字符串类型,值可以是任意数据类型。上述 person 对象一共定义了6个键值对,其中每个键又称为对象的属性,例如, person 的 name 属性为 ‘Bob’ , zipcode 属性为 null 。

要获取一个对象的属性,我们用 对象变量.属性名 的方式:

person.name; // 'Bob'
person.zipcode; // null

变量

变量的概念基本上和初中代数的方程变量是一致的,只是在计算机程序中,变量不仅可以是数字,还可以是任意数据类型。

变量在JavaScript中就是用一个变量名表示,变量名是大小写英文、数字、 $_ 的组合,且不能用数字开头。变量名也不能是JavaScript的关键字,如ifwhile等。申明一个变量用 var 语句,比如:

var a; // 申明了变量a,此时a的值为undefined
var $b = 1; // 申明了变量$b,同时给$b赋值,此时$b的值为1 
var s_007 = '007'; // s_007是一个字符串 
var Answer = true; // Answer是一个布尔值true 
var t = null; // t的值是null

变量名也可以用中文,但是,请不要给自己找麻烦。

在JavaScript中,使用等号= 对变量进行赋值。可以把任意数据类型赋值给变量,同一个变量可以反复赋值,而且可以是不同类型的变量,但是要注意只能用var申明一次,例如:

var a = 123; // a的值是整数123 
a = 'ABC'; // a变为字符串

这种变量本身类型不固定的语言称之为动态语言,与之对应的是静态语言。静态语言在定义变量时必须指定变量类型,如果赋值的时候类型不匹配,就会报错。例如Java是静态语言,赋值语句如下:

int a = 123; // a是整数类型变量,类型用int申明 
a = "ABC"; // 错误:不能把字符串赋给整型变量

和静态语言相比,动态语言更灵活,就是这个原因。

请不要把赋值语句的等号等同于数学的等号。比如下面的代码:

var x = 10;
x = x + 2;

打印变量X,要显示变量的内容,可以用 console.log(x),打开Chrome的控制台就可以看到结果。

var x = 100; 
console.log(x);

使用console.log()代替alert()的好处是可以避免弹出烦人的对话框。

2.4、严格检查模式strict

JavaScript在设计之初,为了方便初学者学习,并不强制要求用 var申明变量。这个设计错误带来了严重的后果:如果一个变量没有通过var申明就被使用,那么该变量就自动被申明为全局变量

i = 10; // i现在是全局变量

在同一个页面的不同的JavaScript文件中,如果都不用var申明,恰好都使用了变量i,将造成变量i互相影响,产生难以调试的错误结果。

使用var 申明的变量则不是全局变量,它的范围被限制在该变量被申明的函数体内(函数的概念将稍后讲解),同名变量在不同的函数体内互不冲突。

为了修补JavaScript这一严重设计缺陷,ECMA在后续规范中推出了strict模式,在strict模式下运行的JavaScript代码,强制通过var 申明变量,未使用 var 申明变量就使用的,将导致运行错误。

启用strict模式的方法是在JavaScript代码的第一行写上:

'use strict';

这是一个字符串,不支持strict模式的浏览器会把它当做一个字符串语句执行,支持strict模式的浏览器将开启strict模式运行JavaScript。

'use strict';
// 如果浏览器支持strict模式,下面的代码将报ReferenceError错误:
abc = 'Hello, world';
console.log(abc);

运行代码,如果浏览器报错,请修复后再运行。如果浏览器不报错,说明你的浏览器太古老了,需要尽快升级。

不用var申明的变量会被视为全局变量,为了避免这一缺陷,所有的JavaScript代码都应该使用strict模式。我们在后面编写的JavaScript代码将全部采用strict模式。

3、数据类型

3.1、字符串

**1、**JavaScript的字符串就是用单引号或双引号括起来的字符表示。

**2、**转义字符

如果'本身也是一个字符,那就可以用""括起来,比如 "I'm OK"包含的字符是I'm,空格, OK这6个字符。

如果字符串内部既包含 '又包含"怎么办?可以用转义字符\ 来标识,比如:

'I\'m \"OK\"!';

转义字符 \ 可以转义很多字符:

\n   //换行
\t   //制表符
\'   // '
\\   // \

ASCII字符可以以 \x##形式的十六进制表示

'\x41'; // 完全等同于 'A'

还可以用 \u####表示一个Unicode字符:

'\u4e2d\u6587'; // 完全等同于 '中文'

**3、**多行字符串编写

//tab上面的反引号
var msg = 
    `hello
	world
	nihao
	wocao`

**4、**模板字符串

    <script>
        'use strict'

        //tab 上面``
        var name = "zhang";
        var age = 3;

        var msg = `nihao, ${name}`;
        console.log(msg)
    </script>
//输出结果nihao,zhang

要把多个字符串连接起来,可以用 + 号连接:

var name = '小明';
var age = 20; 
var message = '你好, ' + name + ', 你今年' + age + '岁了!';
alert(message);

如果有很多变量需要连接,用 + 号就比较麻烦。ES6新增了一种模板字符串,表示方法和上面的多行字符串一样,但是它会自动替换字符串中的变量:

var name = '小明'; 
var age = 20;
var message = `你好, ${name}, 你今年${age}岁了!`; 
alert(message);

**5、**字符串长度

var student = "student";
console.log(student.length);
//输出结果为7

**6、**字符串是不可变的,如果对字符串的某个索引赋值,不会有任何错误,但是,也没有任何效果:

var s = 'Test'; 
s[0] = 'X'; 
alert(s);   // s仍然为'Test'

**7、**JavaScript为字符串提供了一些常用方法,注意,调用这些方法本身不会改变原有字符串的内容,而是返回一个新字符串:

  • toUpperCase() 把一个字符串全部变为大写

    var s = 'Hello';
    s.toUpperCase(); // 返回'HELLO'
    
  • toLowerCase() 把一个字符串全部变为小写

    var s = 'Hello';
    var lower = s.toLowerCase(); // 返回'hello'并赋值给变量lower 
    lower; // 'hello'
    
  • indexOf() 会搜索指定字符串出现的位置

    var s = 'hello, world';
    s.indexOf('world'); // 返回7 
    s.indexOf('World'); // 没有找到指定的子串,返回-1
    
  • substring() 返回指定索引区间的子串

    var s = 'hello, world';
    s.substring(0, 5); // 从索引0开始到5(不包括5),返回'hello' 
    s.substring(7); // 从索引7开始到结束,返回'world'
    

3.2、数组

Array可以包含任意的数据类型

var arr = [1,2,3,4,5,6];
arr.length;// 6

请注意,直接给 Array 的 length 赋一个新的值会导致 Array 大小的变化:

var arr = [1, 2, 3]; 
arr.length; // 3 
arr.length = 6;
arr; // arr变为[1, 2, 3, undefined, undefined, undefined] 
arr.length = 2; 
arr; // arr变为[1, 2]

Array可以通过索引把对应的元素修改为新的值,因此,对Array 的索引进行赋值会直接修改这个Array

var arr = ['A', 'B', 'C'];
arr[1] = 99; 
arr; // arr现在变为['A', 99, 'C']

请注意,如果通过索引赋值时,索引超过了范围,同样会引起 Array大小的变化:

var arr = [1, 2, 3]; 
arr[5] = 'x'; 
arr; // arr变为[1, 2, 3, undefined, undefined, 'x']

大多数其他编程语言不允许直接改变数组的大小,越界访问索引会报错。然而,JavaScript的 Array却不会有任何错误。在编写代码时,不建议直接修改Array 的大小,访问索引时要确保索引不会越界

常用方法

  • indexOf

    与String类似,Array也可以通过 indexOf()来搜索一个指定的元素的位置:

    var arr = [10, 20, '30', 'xyz'];
    arr.indexOf(10); // 元素10的索引为0 
    arr.indexOf(20); // 元素20的索引为1 
    arr.indexOf(30); // 元素30没有找到,返回-1 
    arr.indexOf('30'); // 元素'30'的索引为2
    
  • slice

    slice()就是对应String的substring()版本,它截取 Array 的部分元素,然后返回一个

    新的 Array

    var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G'];
    arr.slice(0, 3); // 从索引0开始,到索引3结束,但不包括索引3: ['A', 'B', 'C'] 
    arr.slice(3); // 从索引3开始到结束: ['D', 'E', 'F', 'G']
    

    注意到 slice()的起止参数包括开始索引,不包括结束索引。

    如果不给 slice()传递任何参数,它就会从头到尾截取所有元素。利用这一点,我们可以很容易地复制一个 Array

    var arr = ['A', 'B', 'C', 'D', 'E', 'F', 'G']; 
    var aCopy = arr.slice(); 
    aCopy; // ['A', 'B', 'C', 'D', 'E', 'F', 'G'] 
    aCopy === arr; // false
    
  • push和pop

  • push()Array 的末尾添加若干元素,pop() 则把 Array的最后一个元素删除掉

    var arr = [1, 2]; 
    arr.push('A', 'B'); // 返回Array新的长度: 4 
    arr; // [1, 2, 'A', 'B'] 
    arr.pop(); // pop()返回'B' 
    arr; // [1, 2, 'A'] 
    arr.pop(); arr.pop(); arr.pop(); // 连续pop 3次 
    arr; // [] 
    arr.pop(); // 空数组继续pop不会报错,而是返回undefined 
    arr; // []
    
  • unshift和shift

    如果要往 Array的头部添加若干元素,使用 unshift()方法, shift()方法则把Array 的第一个元素删掉:

    var arr = [1, 2];
    arr.unshift('A', 'B'); // 返回Array新的长度: 4 
    arr; // ['A', 'B', 1, 2] 
    arr.shift(); // 'A'
    arr; // ['B', 1, 2] 
    arr.shift(); arr.shift(); arr.shift(); // 连续shift 3次
    arr; // [] 
    arr.shift(); // 空数组继续shift不会报错,而是返回undefined
    arr; // []
    
  • sort

    sort()可以对当前Array 进行排序,它会直接修改当前Array的元素位置,直接调用时,按照默认顺序排序

    var arr = ['B', 'C', 'A'];
    arr.sort(); 
    arr; // ['A', 'B', 'C']
    
  • reverse

    reverse()把整个Array的元素给掉个头,也就是反转

    var arr = ['one', 'two', 'three'];
    arr.reverse(); 
    arr; // ['three', 'two', 'one']
    
  • splice

    splice()方法是修改Array 的“万能方法”,它可以从指定的索引开始删除若干元素,然后再从该位置添加若干元素

    var arr = ['Microsoft', 'Apple', 'Yahoo', 'AOL', 'Excite', 'Oracle']; 
    // 从索引2开始删除3个元素,然后再添加两个元素: 
    arr.splice(2, 3, 'Google', 'Facebook'); // 返回删除的元素 ['Yahoo', 'AOL', 'Excite'] 
    arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle'] 
    // 只删除,不添加: 
    arr.splice(2, 2); // ['Google', 'Facebook'] 
    arr; // ['Microsoft', 'Apple', 'Oracle'] 
    // 只添加,不删除: 
    arr.splice(2, 0, 'Google', 'Facebook'); // 返回[],因为没有删除任何元素 
    arr; // ['Microsoft', 'Apple', 'Google', 'Facebook', 'Oracle']
    
  • concat

    concat()方法把当前的 Array和另一个 Array连接起来,并返回一个新的 Array

    var arr = ['A', 'B', 'C']; 
    var added = arr.concat([1, 2, 3]); 
    added; // ['A', 'B', 'C', 1, 2, 3]
    arr; // ['A', 'B', 'C']
    

    请注意, concat()方法并没有修改当前Array ,而是返回了一个新的Array

    实际上, concat()方法可以接收任意个元素和 Array ,并且自动把 Array 拆开,然后全部添加到新的 Array 里:

    var arr = ['A', 'B', 'C']; 
    arr.concat(1, 2, [3, 4]); // ['A', 'B', 'C', 1, 2, 3, 4]
    
  • join

    join()方法是一个非常实用的方法,它把当前 Array的每个元素都用指定的字符串连接起来,然后返回连接后的字符串:

    var arr = ['A', 'B', 'C', 1, 2, 3]; 
    arr.join('-'); // 'A-B-C-1-2-3'
    

    如果Array 的元素不是字符串,将自动转换为字符串后再连接。

  • 多维数组

    如果数组的某个元素又是一个 Array ,则可以形成多维数组,例如:

    var arr = [[1, 2, 3], [400, 500, 600], '-'];
    

    上述Array包含3个元素,其中头两个元素本身也是Array

    练习:如何通过索引取到 500这个值:

    var x = arr[1][2]; 
    console.log(x); // x应该为500
    

3.3、对象

JavaScript的对象是一种无序的集合数据类型,它由若干键值对组成。

JavaScript的对象用于描述现实世界中的某个对象。

var person = { 
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School',
    height: 1.70, 
    weight: 65,
    score: null
}

1、定义一个对象

var 对象名 = { 
    key: 'value',
    key: 'value',
    key: 'value' 
}

2、获取对象的属性

对象.属性

3、由于JavaScript的对象是动态类型,你可以自由地给一个对象添加或删除属性:

var xiaoming = { 
    name: '小明'
};
xiaoming.age; // undefined 
xiaoming.age = 18; // 新增一个age属性 
xiaoming.age; // 18 
delete xiaoming.age; // 删除age属性 
xiaoming.age; // undefined 
delete xiaoming['name']; // 删除name属性 
xiaoming.name; // undefined 
delete xiaoming.school; // 删除一个不存在的school属性也不会报错

如果我们要检测对象是否拥有某一属性,可以用 in 操作符:

var xiaoming = { 
    name: '小明',
    birth: 1990,
    school: 'No.1 Middle School', 
    height: 1.70, 
    weight: 65, 
    score: null
};
'name' in xiaoming; // true 
'grade' in xiaoming; // false

不过要小心,如果用 in 判断一个属性存在,这个属性不一定是 这个对象的,它可能是这个对象继承得到的:

'toString' in xiaoming; // true

因为 toString 定义在 object 对象中,而所有对象最终都会在原型链上指向 object,所以xiaoming 也拥有 toString 属性。

要判断一个属性是否是 xiaoming 自身拥有的,而不是继承得到的,可以用 hasOwnProperty() 方法:

var xiaoming = {
    name: '小明'
};xiaoming.hasOwnProperty('name'); // true 
xiaoming.hasOwnProperty('toString'); // false

3.4、流程控制

  • if判断
var age = 3;
if (age >= 18) { 
    alert('adult'); 
} else if(age >= 6) { 
    alert('teenager'); 
} else { 
    alert('kid');
}
  • for循环

    基础语法

var x = 0; 
var i;
for (i=1; i<=10000; i++) {
    x = x + i;
}
x; // 50005000

遍历数组

var arr = ['Apple', 'Google', 'Microsoft'];
var i, x; 
for (i=0; i<arr.length; i++) { 
    x = arr[i];
    console.log(x);
}

无线循环

var x = 0;
for (;;) { // 将无限循环下去
    if (x > 100) { 
        break; // 通过if判断来退出循环
    }
    x ++;
}

​ for … in , 它可以把一个对象的所有属性依次循环出来:

var o = { 
    name: 'Jack',
    age: 20,
    city: 'Beijing'
};
for (var key in o) { 
    if (o.hasOwnProperty(key)) { 
        console.log(key); // 'name', 'age', 'city' 
    }
}

由于 Array 也是对象,而它的每个元素的索引被视为对象的属性,所以遍历出来是下标

var a = ['A', 'B', 'C'];
for (var i in a) {
    console.log(i); // '0', '1', '2'
    console.log(a[i]); // 'A', 'B', 'C' 
}

请注意, for ... inArray 的循环得到的是String而不是 Number

  • while循环

    基本操作

var x = 0; 
var n = 99;
while (n > 0) {
    x = x + n; 
    n = n - 2;
}
x; // 2500

do…while

var n = 0;
do {
    n = n + 1;
} while (n < 100); 
n; // 100

在编写循环代码时,务必小心编写初始条件和判断条件,尤其是边界值。

特别注意i < 100i <= 100 是不同的判断逻辑。

3.5、Map和Set

JavaScript的默认对象表示方式 { } 可以视为其他语言中的 Map 或 Dictionary 的数据结构,即一组键值对。但是JavaScript的对象有个小问题,就是键必须是字符串。但实际上Number或者其他数据类型作为键也是非常合理的。

为了解决这个问题,最新的ES6规范引入了新的数据类型 Map

  • Map

Map是一组键值对的结构,具有极快的查找速度。

举个例子,假设要根据同学的名字查找对应的成绩,如果用Array实现,需要两个Array

var names = ['Michael', 'Bob', 'Tracy'];
var scores = [95, 75, 85];

给定一个名字,要查找对应的成绩,就先要在names中找到对应的位置,再从scores取出对应的成绩,Array越长,耗时越长。

如果用Map实现,只需要一个“名字”-“成绩”的对照表,直接根据名字查找成绩,无论这个表有多大,查找速度都不会变慢。用JavaScript写一个Map如下:

var m = new Map([['Michael', 95], ['Bob', 75], ['Tracy', 85]]);
m.get('Michael'); // 95

初始化Map需要一个二维数组,或者直接初始化一个空MapMap具有以下方法:

var m = new Map(); // 空Map 
m.set('Adam', 67); // 添加新的key-value
m.set('Bob', 59);
m.has('Adam'); // 是否存在key 'Adam': true
m.get('Adam'); // 67
m.delete('Adam'); // 删除key 'Adam' 
m.get('Adam'); // undefined

由于一个key只能对应一个value,所以,多次对一个key放入value,后面的值会把前面的值冲掉:

var m = new Map();
m.set('Adam', 67);
m.set('Adam', 88);
m.get('Adam'); // 88
  • set

SetMap 类似,也是一组key的集合,但不存储value。由于key不能重复,所以,在 Set 中,没有重复的key。

var s1 = new Set(); // 空Set 
var s2 = new Set([1, 2, 3]); // 含1, 2, 3

重复元素在 Set 中自动被过滤:

var s = new Set([1, 2, 3, 3, '3']); 
s; // Set {1, 2, 3, "3"}

注意数字 3和字符串'3'是不同的元素。

通过 add(key)方法可以添加元素到Set中,可以重复添加,但不会有效果:

s.add(4); 
s; // Set {1, 2, 3, 4} 
s.add(4); 
s; // 仍然是 Set {1, 2, 3, 4}

通过 delete(key)方法可以删除元素:

var s = new Set([1, 2, 3]);
s; // Set {1, 2, 3} 
s.delete(3); 
s; // Set {1, 2}

3.6、Iterable

遍历 Array 可以采用下标循环,遍历MapSet就无法使用下标。

为了统一集合类型,ES6标准引入了新的 iterable类型,ArrayMapSet属于;

具有iterable 类型的集合可以通过新的 for ... of 循环来遍历。

  • 遍历集合
var a = ['A', 'B', 'C']; 
var s = new Set(['A', 'B', 'C']); 
var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]); 

for (var x of a) { // 遍历Array 
    console.log(x);
}
for (var x of s) { // 遍历Set 
    console.log(x);
}
for (var x of m) { // 遍历Map 
    console.log(x[0] + '=' + x[1]); 
}

更好的方式是直接使用iterable 内置的 forEach方法,它接收一个函数,每次迭代就自动回调该函数。以 Array为例

a.forEach(function (element, index, array) { 
    // element: 指向当前元素的值 
    // index: 指向当前索引 
    // array: 指向Array对象本身 
    console.log(element + ', index = ' + index);
});

forEach() 方法是ES5.1标准引入的,你需要测试浏览器是否支持。

Set 没有索引,因此回调函数的前两个参数都是元素本身:

var s = new Set(['A', 'B', 'C']); 
s.forEach(function (element, sameElement, set) { 
    console.log(element);
});

Map 的回调函数参数依次为 valuekeymap 本身:

var m = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
m.forEach(function (value, key, map) { 
    console.log(value);
});

4、函数

4.1、定义函数

  • 定义方式一

绝对值函数

function abs(x) {
    if (x >= 0) {
        return x;
    } else { 
        return -x; 
    }
}

一旦执行到 return 时,函数就执行完毕,并将结果返回。

如果没有 return 语句,函数执行完毕后也会返回结果,只是结果为 undefined 。

  • 定义方式二
var abs = function (x) { 
    if (x >= 0) {
        return x;
    } else { 
        return -x; 
    } 
};

在这种方式下,function (x) { ... }是一个匿名函数,它没有函数名。但是,这个匿名函数赋值给了变量 abs ,所以,通过变量 abs就可以调用该函数。

上述两种定义完全等价,注意第二种方式按照完整语法需要在函数体末尾加一个; ,表示赋值语句结束。

  • 调用函数

调用函数时,按顺序传入参数即可:

abs(10); // 返回10
abs(-9); // 返回9

由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的参数多也没有问题,虽然函数内部并不需要这些参数:

abs(10, 'blablabla'); // 返回10 
abs(-9, 'haha', 'hehe', null); // 返回9

传入的参数比定义的少也没有问题:

abs(); // 返回NaN

此时abs(x)函数的参数x 将收到 undefined ,计算结果为 NaN

要避免收到 undefined ,可以对参数进行检查:

function abs(x) { 
    if (typeof x !== 'number') { 
        throw 'Not a number';
    }
    if (x >= 0) {
        return x;
    } else { 
        return -x; 
    } 
}
  • arguments

    JavaScript还有一个免费赠送的关键字 arguments ,它只在函数内部起作用,并且永远指向当前函数的调用者传入的所有参数。

function foo(x) { 
    console.log('x = ' + x); // 10
    for (var i=0; i<arguments.length; i++) {
        console.log('arg ' + i + ' = ' + arguments[i]);// 10, 20, 30
    } 
}
foo(10, 20, 30);

利用 arguments ,你可以获得调用者传入的所有参数。也就是说,即使函数不定义任何参数,还是可以拿到参数的值:

function abs() {
    if (arguments.length === 0) { 
        return 0;
    }var x = arguments[0];
    return x >= 0 ? x : -x;
}

abs(); // 0 
abs(10); // 10
abs(-9); // 9

实际上arguments最常用于判断传入参数的个数。你可能会看到这样的写法:

// foo(a, [b], c)
// 接收2~3个参数,b是可选参数,如果只传2个参数,b默认为null:
function foo(a, b, c) { 
    if (arguments.length === 2) { 
        // 实际拿到的参数是a和b,c为undefined
        c = b; // 把b赋给c 
        b = null; // b变为默认值
    }
    // ...
}
  • rest

由于JavaScript函数允许接收任意个参数,于是我们就不得不用 arguments来获取所有参数:

function foo(a, b) {
    var i, rest = []; 
    if (arguments.length > 2) {
        for (i = 2; i<arguments.length; i++) { 
            rest.push(arguments[i]); 
        }
    }
    console.log('a = ' + a); 
    console.log('b = ' + b); 
    console.log(rest); 
}

为了获取除了已定义参数ab之外的参数,我们不得不用arguments ,并且循环要从索引2开始以便排除前两个参数,这种写法很别扭,只是为了获得额外的rest参数,有没有更好的方法?

ES6标准引入了rest参数,上面的函数可以改写为:

function foo(a, b, ...rest) { 
    console.log('a = ' + a); 
    console.log('b = ' + b); 
    console.log(rest);
}
foo(1, 2, 3, 4, 5); 
// 结果: 
// a = 1
// b = 2 
// Array [ 3, 4, 5 ] 

foo(1); 
// 结果:
// a = 1 
// b = undefined 
// Array []

rest参数只能写在最后,前面用...标识,从运行结果可知,传入的参数先绑定 ab,多余的参数以数组形式交给变量 rest,所以,不再需要 arguments 我们就获取了全部参数。

4.2、变量的作用域

变量的作用域

在JavaScript中,用 var申明的变量实际上是有作用域的。

如果一个变量在函数体内部申明,则该变量的作用域为整个函数体,在函数体外不可引用该变量:

'use strict';
function foo() {
    var x = 1;
    x = x + 1;
}

x = x + 2; // ReferenceError! 无法在函数体外引用变量x

如果两个不同的函数各自申明了同一个变量,那么该变量只在各自的函数体内起作用。换句话说,不同函数内部的同名变量互相独立,互不影响:

'use strict'; 

function foo() {
    var x = 1; 
    x = x + 1;
}

function bar() { 
    var x = 'A';
    x = x + 'B';
}

由于JavaScript的函数可以嵌套,此时,内部函数可以访问外部函数定义的变量,反过来则不行:

'use strict';

function foo() {
    var x = 1;
    function bar() {
        var y = x + 1; // bar可以访问foo的变量x!
    }
    var z = y + 1; // ReferenceError! foo不可以访问bar的变量y!
}

如果内部函数和外部函数的变量名重名怎么办?来测试一下:

function foo() { 
    var x = 1;
    function bar() { 
        var x = 'A';
        console.log('x in bar() = ' + x); // 'A'
    }
    console.log('x in foo() = ' + x); // 1 
    bar(); 
}

foo();

这说明JavaScript的函数在查找变量时从自身函数定义开始,从“内”向“外”查找。如果内部函数定义了与外部函数重名的变量,则内部函数的变量将“屏蔽”外部函数的变量。

变量提升

JavaScript的函数定义有个特点,它会先扫描整个函数体的语句,把所有申明的变量“提升”到函数顶部:

'use strict'; 

function foo() {
    var x = 'Hello, ' + y;
    console.log(x); 
    var y = 'Bob';
}
foo();

结果:Hello, undefifined , 说明 y的值为undefifined

这正是因为JavaScript引擎自动提升了变量y的声明,但不会提升变量y的赋值。

对于上述 foo() 函数,JavaScript引擎看到的代码相当于:

function foo() {
    var y; // 提升变量y的申明,此时y为undefined
    var x = 'Hello, ' + y; 
    console.log(x);
    y = 'Bob'; 
}

由于JavaScript的这一怪异的“特性”,我们在函数内部定义变量时,请严格遵守“在函数内部首先申明所有变量”这一规则。最常见的做法是用一个var申明函数内部用到的所有变量:

function foo() { 
    var 
    	x = 1, // x初始化为1 
        y = x + 1, // y初始化为2
        z, i; // z和i为undefined 
    // 其他语句: 
    for (i=0; i<100; i++) {
        ... 
    } 
}

全局作用域

不在任何函数内定义的变量就具有全局作用域。实际上,JavaScript默认有一个全局对象window ,全局作用域的变量实际上被绑定到 window的一个属性:

'use strict'; 

var course = 'Learn JavaScript';
alert(course); // 'Learn JavaScript' 
alert(window.course); // 'Learn JavaScript

因此,直接访问全局变量course 和访问window.course 是完全一样的。

因此,顶层函数的定义也被视为一个全局变量,并绑定到 window 对象:

'use strict';

function foo() {
    alert('foo');
}

foo(); // 直接调用foo() 
window.foo(); // 通过window.foo()调用

进一步大胆地猜测,我们每次直接调用的 alert()函数其实也是window 的一个变量:

'use strict';

window.alert('调用window.alert()');
// 把alert保存到另一个变量:
var old_alert = window.alert; 
// 给alert赋一个新函数: 
window.alert = function () {} 
alert('无法用alert()显示了!'); 
// 恢复alert: 
window.alert = old_alert;
alert('又可以用alert()了!');

这说明JavaScript实际上只有一个全局作用域。任何变量(函数也视为变量),如果没有在当前函数作用域中找到,就会继续往上查找,最后如果在全局作用域中也没有找到,则报 ReferenceError 错误。

全局变量会绑定到 window 上,不同的JavaScript文件如果使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突,并且很难被发现。减少冲突的一个方法是把自己的所有变量和函数全部绑定到一个全局变量中。例如:

// 唯一的全局变量MYAPP: 
var MYAPP = {};

// 其他变量:
MYAPP.name = 'myapp'; 
MYAPP.version = 1.0;

// 其他函数: 
MYAPP.foo = function () {
    return 'foo';
};

把自己的代码全部放入唯一的名字空间 MYAPP中,会大大减少全局变量冲突的可能。许多著名的JavaScript库都是这么干的:jQuery,YUI,underscore等等。

局部作用域

由于JavaScript的变量作用域实际上是函数内部,我们在 for循环等语句块中是无法定义具有局部作用域的变量的:

'use strict';

function foo() {
    for (var i=0; i<100; i++) {
        //
    }
    i += 100; // 仍然可以引用变量i
}

为了解决块级作用域,ES6引入了新的关键字 let,用let 替代var 可以申明一个块级作用域的变量:

'use strict'; 

function foo() {
    var sum = 0; 
    for (let i=0; i<100; i++) { 
        sum += i;
    }
    // SyntaxError:
    i += 1;
}

常量

由于 varlet 申明的是变量,如果要申明一个常量,在ES6之前是不行的,我们通常用全部大写的变量来表示“这是一个常量,不要修改它的值”:

var PI = 3.14;

ES6标准引入了新的关键字 const 来定义常量,constlet都具有块级作用域:

'use strict';

const PI = 3.14;
PI = 3; // 某些浏览器不报错,但是无效果! 
PI; // 3.14

4.3、方法

定义方法

在一个对象中绑定函数,称为这个对象的方法。

在JavaScript中,对象的定义是这样的:

var xiaoming = {
    name: '小明', 
    birth: 1990 
};

但是,如果我们给xiaoming 绑定一个函数,就可以做更多的事情。比如,写个age() 方法,返回 xiaoming的年龄:

var xiaoming = { 
    name: '小明', 
    birth: 1990,
    age: function () { 
        var y = new Date().getFullYear(); 
        return y - this.birth; 
    } 
};

xiaoming.age; // function xiaoming.age() 
xiaoming.age(); // 今年调用是25,明年调用就变成26了

绑定到对象上的函数称为方法,和普通函数也没啥区别,但是它在内部使用了一个this关键字,这个东东是什么?

在一个方法内部, this 是一个特殊变量,它始终指向当前对象,也就是xiaoming这个变量。所以, this.birth 可以拿到xiaomingbirth 属性。

拆开写:

function getAge() { 
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = {
    name: '小明',
    birth: 1990, 
    age: getAge 
};

xiaoming.age(); // 25, 正常结果 
getAge(); // NaN

单独调用函数 getAge()怎么返回了 NaN?请注意,我们已经进入到了JavaScript的一个大坑里。JavaScript的函数内部如果调用了this,那么这个this到底指向谁?

如果以对象的方法形式调用,比如 xiaoming.age(),该函数的 this指向被调用的对象,也就是 xiaoming,这是符合我们预期的。

如果单独调用函数,比如getAge() ,此时,该函数的 this指向全局对象,也就是window

apply

不过,我们还是可以控制this的指向的!

要指定函数的this指向哪个对象,可以用函数本身的apply 方法,它接收两个参数,第一个参数就是需要绑定的this变量,第二个参数是 Array ,表示函数本身的参数。

function getAge() { 
    var y = new Date().getFullYear();
    return y - this.birth;
}

var xiaoming = { 
    name: '小明', 
    birth: 1990, 
    age: getAge
};

xiaoming.age(); // 25
getAge.apply(xiaoming, []); // 25, this指向xiaoming, 参数为空

5、内部对象

5.1、标准对象

是某些对象还是和其他对象不太一样。为了区分对象的类型,我们用 typeof 操作符获取对象的类型,它总是返回一个字符串:

typeof 123; // 'number' 
typeof NaN; // 'number' 
typeof 'str'; // 'string' 
typeof true; // 'boolean' 
typeof undefined; // 'undefined' 
typeof Math.abs; // 'function' 
typeof null; // 'object' 
typeof []; // 'object' 
typeof {}; // 'object'

5.2、Date

在JavaScript中,Date对象用来表示日期和时间。

要获取系统当前时间,用:

var now = new Date(); 
now; // Wed Jun 24 2015 19:49:22 GMT+0800 (CST) 
now.getFullYear(); // 2015, 年份 
now.getMonth(); // 5, 月份,注意月份范围是0~11,5表示六月 
now.getDate(); // 24, 表示24号 
now.getDay(); // 3, 表示星期三
now.getHours(); // 19, 24小时制 
now.getMinutes(); // 49, 分钟
now.getSeconds(); // 22, 秒 
now.getMilliseconds(); // 875, 毫秒数
now.getTime(); // 1435146562875, 以number形式表示的时间戳

注意,当前时间是浏览器从本机操作系统获取的时间,所以不一定准确,因为用户可以把当前时间设定为任何值。

时区

Date 对象表示的时间总是按浏览器所在时区显示的,不过我们既可以显示本地时间,也可以显示调整后的UTC时间:

var d = new Date(1435146562875); 
d.toLocaleString(); // '2015/6/24 下午7:49:22',本地时间(北京时区+8:00),显示的字 符串与操作系统设定的格式有关 
d.toUTCString(); // 'Wed, 24 Jun 2015 11:49:22 GMT',UTC时间,与本地时间相差8小时

那么在JavaScript中如何进行时区转换呢?实际上,只要我们传递的是一个number 类型的时间戳,我们就不用关心时区转换。任何浏览器都可以把一个时间戳正确转换为本地时间。

时间戳是个什么东西?时间戳是一个自增的整数,它表示从1970年1月1日零时整的GMT时区开始的那一刻,到现在的毫秒数。假设浏览器所在电脑的时间是准确的,那么世界上无论哪个时区的电脑,它们此刻产生的时间戳数字都是一样的,所以,时间戳可以精确地表示一个时刻,并且与时区无关。

所以,我们只需要传递时间戳,或者把时间戳从数据库里读出来,再让JavaScript自动转换为当地时间就可以了。

要获取当前时间戳,可以用:

if (Date.now) {
    console.log(Date.now()); // 老版本IE没有now()方法
} else { 
    console.log(new Date().getTime()); 
}

5.2、JSON

  • JSON(JavaScript Object Notation, JS 对象标记) 是一种轻量级的数据交换格式,目前使用特别广泛。
  • 采用完全独立于编程语言的文本格式来存储和表示数据。
  • 简洁和清晰的层次结构使得 JSON 成为理想的数据交换语言。
  • 易于人阅读和编写,同时也易于机器解析和生成,并有效地提升网络传输效率。

在 JavaScript 语言中,一切都是对象。因此,任何JavaScript 支持的类型都可以通过 JSON 来表示,例如字符串、数字、对象、数组等。看看他的要求和语法格式:

  • 对象表示为键值对,数据由逗号分隔
  • 花括号保存对象
  • 方括号保存数组

JSON 键值对是用来保存 JavaScript 对象的一种方式,和 JavaScript 对象的写法也大同小异,键/值对组合中的键名写在前面并用双引号 “” 包裹,使用冒号 : 分隔,然后紧接着值:

{"name": "QinJiang"}
{"age": "3"}
{"sex": "男"}

很多人搞不清楚 JSON 和 JavaScript 对象的关系,甚至连谁是谁都不清楚。其实,可以这么理解:

  • JSON 是 JavaScript 对象的字符串表示法,它使用文本表示一个 JS 对象的信息,本质是一个字符串。

    var obj = {a: 'Hello', b: 'World'}; //这是一个对象,注意键名也是可以使用引号包裹 的
    var json = '{"a": "Hello", "b": "World"}'; //这是一个 JSON 字符串,本质是一个 字符串
    

JSON 和 JavaScript 对象互转

  • 要实现从JSON字符串转换为JavaScript 对象,使用 JSON.parse() 方法:

    var obj = JSON.parse('{"a": "Hello", "b": "World"}'); 
    //结果是 {a: 'Hello', b: 'World'}
    
  • 要实现从JavaScript 对象转换为JSON字符串,使用 JSON.stringify() 方法:

    var json = JSON.stringify({a: 'Hello', b: 'World'});
    //结果是 '{"a": "Hello", "b": "World"}'
    

代码测试

  1. 新建一个 json-1.html , 编写测试内容
DOCTYPE html> 
<html lang="en"> 
<head> 
<meta charset="UTF-8">
<title>JSON_秦疆title> 
head>
<body> 
        
<script type="text/javascript">
   //编写一个js的对象 
   var user = { 
       name:"秦疆",
       age:3,
       sex:"男"
   };
    //将js对象转换成json字符串 
    var str = JSON.stringify(user);
    console.log(str); 
    
    //将json字符串转换为js对象 
    var user2 = JSON.parse(str); 
    console.log(user2.age,user2.name,user2.sex);
script> 
body> 
html>
  1. 在IDEA中使用浏览器打开,查看控制台输出!

6、面向对象编程

面向对象编程

JavaScript的面向对象编程和大多数其他语言如Java、C#的面向对象编程都不太一样。如果你熟悉Java或C#,很好,你一定明白面向对象的两个基本概念:

  1. 类:类是对象的类型模板,例如,定义Student类来表示学生,类本身是一种类型,Student 表示学生类型,但不表示任何具体的某个学生;

  2. 实例:实例是根据类创建的对象,例如,根据 Student 类可以创建出xiaomingxiaohongxiaojun等多个实例,每个实例表示一个具体的学生,他们全都属于Student类型。

所以,类和实例是大多数面向对象编程语言的基本概念。

不过,在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。

原型是指当我们想要创建xiaoming 这个具体的学生时,我们并没有一个Student类型可用。那怎么办?恰好有这么一个现成的对象:

var robot = { 
    name: 'Robot',
    height: 1.6,
    run: function () {
        console.log(this.name + ' is running...');
    } 
};

我们看这个robot对象有名字,有身高,还会跑,有点像小明,干脆就根据它来“创建”小明得了!于是我们把它改名为 Student ,然后创建出 xiaoming

var Student = { 
    name: 'Robot',
    height: 1.2, 
    run: function () {
        console.log(this.name + ' is running...');
    } 
};

var xiaoming = { 
    name: '小明'
};

xiaoming.__proto__ = Student;

注意最后一行代码把xiaoming 的原型指向了对象 Student ,看上去xiaoming仿佛是从Student继承下来的:

xiaoming.name; // '小明' 
xiaoming.run(); // 小明 is running...

xiaoming 有自己的name属性,但并没有定义 run()方法。不过,由于小明是从 Student继承而来,只要 Studentrun()方法,xiaoming也可以调用:

JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有对象都是实例,所谓继承关系不过是把一个对象的原型指向另一个对象而已。

如果你把 xiaoming 的原型指向其他对象:

var Bird = { 
    fly: function () { 
        console.log(this.name + ' is flying...'); 
    } 
};

xiaoming.__proto__ = Bird;

现在 xiaoming已经无法 run()了,他已经变成了一只鸟:

xiaoming.fly(); // 小明 is flying...

在JavaScrip代码运行时期,你可以把 xiaomingStudent 变成 Bird ,或者变成任何对象。

class继承

在上面的章节中我们看到了JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。

新的关键字 class从ES6开始正式被引入到JavaScript中。class的目的就是让定义类更简单。我们先回顾用函数实现Student的方法:

function Student(name) {
    this.name = name;
}

// 现在要给这个Student新增一个方法 

Student.prototype.hello = function () { 
    alert('Hello, ' + this.name + '!');
}

如果用新的class关键字来编写 Student ,可以这样写:

class Student { 
    constructor(name) { 
        this.name = name;
    }
    
    hello() { 
        alert('Hello, ' + this.name + '!');
    }
}

比较一下就可以发现, class 的定义包含了构造函数 constructor和定义在原型对象上的函数hello()(注意没有function关键字),这样就避免了 Student.prototype.hello = function () {...}这样分散的代码。

最后,创建一个 Student对象代码和前面章节完全一样:

var xiaoming = new Student('小明'); 
xiaoming.hello();

class 定义对象的另一个巨大的好处是继承更方便了。想一想我们从 Student 派生一个PrimaryStudent需要编写的代码量。现在,原型继承的中间对象,原型对象的构造函数等等都不需要考虑了,直接通过 extends来实现:

class PrimaryStudent extends Student {
    constructor(name, grade) { 
        super(name); // 记得用super调用父类的构造方法!
        this.grade = grade;
    }
    
    myGrade() { 
        alert('I am at grade ' + this.grade);
    }
}

7、操作BOM对象(重点)

7.1、浏览器

由于JavaScript的出现就是为了能在浏览器中运行,所以,浏览器自然是JavaScript开发者必须要关注的。

目前主流的浏览器分这么几种:

  • IE 6~11:国内用得最多的IE浏览器,历来对W3C标准支持差。从IE10开始支持ES6标准;
  • Chrome:Google出品的基于Webkit内核浏览器,内置了非常强悍的JavaScript引擎——V8。由于Chrome一经安装就时刻保持自升级,所以不用管它的版本,最新版早就支持ES6了;
  • Safari:Apple的Mac系统自带的基于Webkit内核的浏览器,从OS X 10.7 Lion自带的6.1版本开始支持ES6,目前最新的OS X 10.11 El Capitan自带的Safari版本是9.x,早已支持ES6;
  • Firefox:Mozilla自己研制的Gecko内核和JavaScript引擎OdinMonkey。早期的Firefox按版本发布,后来终于聪明地学习Chrome的做法进行自升级,时刻保持最新;
  • 移动设备上目前iOS和Android两大阵营分别主要使用Apple的Safari和Google的Chrome,由于两者都是Webkit核心,结果HTML5首先在手机上全面普及(桌面绝对是Microsoft拖了后腿),对JavaScript的标准支持也很好,最新版本均支持ES6。

另外还要注意识别各种国产浏览器,如某某安全浏览器,某某旋风浏览器,它们只是做了一个壳,其核心调用的是IE,也有号称同时支持IE和Webkit的“双核”浏览器。

不同的浏览器对JavaScript支持的差异主要是,有些API的接口不一样,比如AJAX,File接口。对于ES6标准,不同的浏览器对各个特性支持也不一样。

在编写JavaScript的时候,就要充分考虑到浏览器的差异,尽量让同一份JavaScript代码能运行在不同的浏览器中。

JavaScript可以获取浏览器提供的很多对象,并进行操作。

7.2、window

window 对象不但充当全局作用域,而且表示浏览器窗口。

window对象有 innerWidthinnerHeight 属性,可以获取浏览器窗口的内部宽度和高度。内部宽高是指除去菜单栏、工具栏、边框等占位元素后,用于显示网页的净宽高。

兼容性:IE<=8不支持。

'use strict';

// 可以调整浏览器窗口大小试试: 
console.log('window inner size: ' + window.innerWidth + ' x ' + window.innerHeight);

对应的,还有一个 outerWidthouterHeight 属性,可以获取浏览器窗口的整个宽高。

7.3、navigator

navigator 对象表示浏览器的信息,最常用的属性包括:

  • navigator.appName:浏览器名称;
  • navigator.appVersion:浏览器版本;
  • navigator.language:浏览器设置的语言;
  • navigator.platform:操作系统类型;
  • navigator.userAgent:浏览器设定的 User-Agent 字符串。
'use strict'; 

console.log('appName = ' + navigator.appName);
console.log('appVersion = ' + navigator.appVersion);
console.log('language = ' + navigator.language); 
console.log('platform = ' + navigator.platform); 
console.log('userAgent = ' + navigator.userAgent);

请注意, navigator的信息可以很容易地被用户修改,所以JavaScript读取的值不一定是正确的。很多初学者为了针对不同浏览器编写不同的代码,喜欢用if判断浏览器版本,例如:

var width; 
if (getIEVersion(navigator.userAgent) < 9) { 
    width = document.body.clientWidth;
} else { 
    width = window.innerWidth; 
}

但这样既可能判断不准确,也很难维护代码。正确的方法是充分利用JavaScript对不存在属性返回undefined的特性,直接用短路运算符||计算:

var width = window.innerWidth || document.body.clientWidth;

7.4、screen

screen 对象表示屏幕的信息,常用的属性有:

  • screen.width:屏幕宽度,以像素为单位;
  • screen.height:屏幕高度,以像素为单位;
  • screen.colorDepth:返回颜色位数,如8、16、24。
console.log('Screen size = ' + screen.width + ' x ' + screen.height);

7.5、location

location 对象表示当前页面的URL信息。例如,一个完整的URL:

http://www.example.com:8080/path/index.html?a=1&b=2#TOP

可以用 location.href 获取。要获得URL各个部分的值,可以这么写:

location.protocol; // 'http'
location.host; // 'www.example.com' 
location.port; // '8080' 
location.pathname; // '/path/index.html' 
location.search; // '?a=1&b=2' 
location.hash; // 'TOP'

要加载一个新页面,可以调用location.assign()。如果要重新加载当前页面,调用location.reload()方法非常方便。

7.5、document

document 对象表示当前页面。由于HTML在浏览器中以DOM形式表示为树形结构, document 对象就是整个DOM树的根节点。

documenttitle 属性是从HTML文档中的xxx读取的,但是可以动态改变:

document.title = '狂神说Java!';

请观察浏览器窗口标题的变化。

要查找DOM树的某个节点,需要从 document对象开始查找。最常用的查找是根据ID和Tag Name。我们先准备HTML数据:

<dl id="code-menu" style="border:solid 1px #ccc;padding:6px;"> 
    <dt>Javadt> 
	<dd>Springdd> 
	<dt>Pythondt> 
	<dd>Djangodd> 
	<dt>Linuxdt> 
	<dd>Dockerdd>
dl>

document 对象提供的 getElementById()getElementsByTagName()可以按ID获得一个DOM节点和按Tag名称获得一组DOM节点:

var menu = document.getElementById('code-menu'); 
var drinks = document.getElementsByTagName('dt');
var i, s;

s = '提供的饮料有:';
for (i=0; i<drinks.length; i++) {
    s = s + drinks[i].innerHTML + ','; 
}
console.log(s);

document对象还有一个 cookie属性,可以获取当前页面的Cookie

Cookie是由服务器发送的key-value标示符。因为HTTP协议是无状态的,但是服务器要区分到底是哪个用户发过来的请求,就可以用Cookie来区分。当一个用户成功登录后,服务器发送一个Cookie给浏览器,例如 user=ABC123XYZ(加密的字符串)… ,此后,浏览器访问该网站时,会在请求头附上这个Cookie,服务器根据Cookie即可区分出用户。

Cookie还可以存储网站的一些设置,例如,页面显示的语言等等。

JavaScript可以通过 document.cookie 读取到当前页面的Cookie:

document.cookie; // 'v=123; remember=true; prefer=zh'

由于JavaScript能读取到页面的Cookie,而用户的登录信息通常也存在Cookie中,这就造成了巨大的安全隐患,这是因为在HTML页面中引入第三方的JavaScript代码是允许的:

<!--www.example.com-->
<html>
    <head>
    	<script src="http://www.foo.com/jquery.js"></script>
	</head>
    ...
</html>

如果引入的第三方的JavaScript中存在恶意代码,则 www.foo.com 网站将直接获取到www.example.com 网站的用户登录信息。

为了解决这个问题,服务器在设置Cookie时可以使用 httpOnly ,设定了 httpOnly 的Cookie将不能被JavaScript读取。这个行为由浏览器实现,主流浏览器均支持 httpOnly 选项,IE从IE6 SP1开始支持。

为了确保安全,服务器端在设置Cookie时,应该始终坚持使用 httpOnly

7.7、history

history 对象保存了浏览器的历史记录,JavaScript可以调用history对象的 back()forward () ,相当于用户点击了浏览器的“后退”或“前进”按钮。

这个对象属于历史遗留对象,对于现代Web页面来说,由于大量使用AJAX和页面交互,简单粗暴地调用 history.back() 可能会让用户感到非常愤怒。

新手开始设计Web页面时喜欢在登录页登录成功时调用 history.back() ,试图回到登录前的页面。这是一种错误的方法。

任何情况,你都不应该使用history 这个对象了。

8、操作DOM对象(重点)

8.1、选择器

由于HTML文档被浏览器解析后就是一棵DOM树,要改变HTML的结构,就需要通过JavaScript来操作DOM。

始终记住DOM是一个树形结构。操作一个DOM节点实际上就是这么几个操作:

  • 更新:更新该DOM节点的内容,相当于更新了该DOM节点表示的HTML的内容;
  • 遍历:遍历该DOM节点下的子节点,以便进行进一步操作;
  • 添加:在该DOM节点下新增一个子节点,相当于动态增加了一个HTML节点;
  • 删除:将该节点从HTML中删除,相当于删掉了该DOM节点的内容以及它包含的所有子节点。

在操作一个DOM节点前,我们需要通过各种方式先拿到这个DOM节点。最常用的方法是document.getElementById()document.getElementsByTagName() ,以及CSS选择器document.getElementsByClassName()

由于ID在HTML文档中是唯一的,所以 document.getElementById() 可以直接定位唯一的一个DOM

document.getElementsByTagName()document.getElementsByClassName()总是返回一组DOM节点。要精确地选择DOM,可以先定位父节点,再从父节点开始选择,以缩小范围。

// 返回ID为'test'的节点:
var test = document.getElementById('test'); 

// 先定位ID为'test-table'的节点,再返回其内部所有tr节点:
var trs = document.getElementById('test-table').getElementsByTagName('tr'); 

// 先定位ID为'test-div'的节点,再返回其内部所有class包含red的节点: 
var reds = document.getElementById('test- div').getElementsByClassName('red'); 

// 获取节点test下的所有直属子节点: 
var cs = test.children; 

// 获取节点test下第一个、最后一个子节点:
var first = test.firstElementChild; var last = test.lastElementChild;

第二种方法是使用querySelector()querySelectorAll(),需要了解selector语法,然后使用条件来获取节点,更加方便:

// 通过querySelector获取ID为q1的节点: 
var q1 = document.querySelector('#q1'); 

// 通过querySelectorAll获取q1节点内的符合条件的所有节点:
var ps = q1.querySelectorAll('div.highlighted > p');

注意:低版本的IE<8不支持querySelectorquerySelectorAll 。IE8仅有限支持。

8.2、更新DOM

拿到一个DOM节点后,我们可以对它进行更新。

可以直接修改节点的文本,方法有两种:

一种是修改innerHTML属性,这个方式非常强大,不但可以修改一个DOM节点的文本内容,还可以直接通过HTML片段修改DOM节点内部的子树:

// 获取

...

var p = document.getElementById('p-id'); // 设置文本为abc: p.innerHTML = 'ABC'; //

ABC

// 设置HTML: p.innerHTML = 'ABC RED XYZ'; //

...

的内部结构已修改

innerHTML时要注意,是否需要写入HTML。如果写入的字符串是通过网络拿到了,要注意对字符编码来避免XSS攻击。

第二种是修改innerText属性,这样可以自动对字符串进行HTML编码,保证无法设置任何HTML标签:

// 获取

...

var p = document.getElementById('p-id'); // 设置文本: p.innerText = ''; // HTML被自动编码,无法设置一个

你可能感兴趣的:(html5,css,JavaScript,开发语言,前端,javascript)