一、初识 JavaScript
1、JavaScript 是什么
JavaScript(简称 JS)是世界上最流行的编程语言之一。是一个脚本语言 ,通过解释器运行
主要在客户端(浏览器)上运行,现在也可以基于 node.js 在服务器端运行。
JavaScript 最初只是为了完成简单的表单验证(验证数据合法性),结果后来不小心就火了。当前 JavaScript 已经成为了一个通用的编程语言。
JavaScript 的能做的事情:
- 网页开发(更复杂的特效和用户交互)
- 网页游戏开发
- 服务器开发(node.js)
- 桌面程序开发(Electron、VSCode 就是这么来的)
- 手机 app 开发
2、发展历史
JavaScript 之父布兰登·艾奇(Brendan Eich)
1995 年,用 10 天时间完成 JS 的设计(由于设计时间太短,语言的一些细节考虑得不够严谨,导致后来很长一段时间,Javascript 写出来的程序混乱不堪)。
最初在网景公司,命名为 LiveScript。一般认为,当时 Netscape 之所以将 LiveScript 命名为 JavaScript,是因为 Java 是当时最流行的编程语言,带有 "Java" 的名字有助于这门新生语言的传播。其实 Java 和 JavaScript 之间的语法风格相去甚远。
JavaScript 发展历史可参考:Javascript诞生记 - 阮一峰的网络日志
3、JavaScript 和 HTML 和 CSS 之间的关系

- HTML:网页的结构(骨)
- CSS:网页的表现(皮)
- JavaScript:网页的行为(魂)
4、JavaScript 运行过程
- 编写的代码 是保存在文件中的,也就是存储在硬盘(外存上)。
- 双击 .html 文件浏览器(应用程序)就会读取文件 ,把文件内容加载到内存中(数据流向:硬盘 => 内存)。
- 浏览器会解析 用户编写的代码,把代码翻译成二进制 的,能让计算机识别的指令(解释器的工作)。
- 得到的二进制指令 会被 CPU加载并执行(数据流向:内存 => CPU)。

浏览器 分成**渲染引擎****+**JS 引擎
- 渲染引擎:解析 html + CSS,俗称 "内核"
- JS 引擎:也就是 JS 解释器。典型的就是 Chrome 中内置的 V8
JS 引擎逐行读取 JS 代码内容,然后解析成二进制指令再执行。
5、JavaScript 的组成
- ECMAScript(简称 ES):JavaScript 语法
- DOM:页面文档对象模型,对页面中的元素进行操作
- BOM:浏览器对象模型,对浏览器窗口进行操作
光有 JS 语法,只能写一些基础的逻辑流程。要想完成更复杂的任务,完成和浏览器以及页面的交互,那么就需要 DOM API 和 BOM API,这主要指在浏览器端运行的 JS。如果是运行在服务端的 JS,则需要使用 node.js 的 API,就不太需要关注 DOM 和 BOM。
(1)ECMAScript
这是一套 "标准",无论是什么样的 JS 引擎都要遵守这个标准来实现。
二、前置知识
1、第一个程序
javascript
<script>
alert("你好!");
</script>
JavaScript 代码可以嵌入到 HTML 的 script 标签中。
2、JavaScript 的书写形式
(1)行内式
直接嵌入到 html 元素内部:


注意:JS 中字符串常量可以使用单引号表示,也可以使用双引号表示。HTML 中推荐使用双引号,JS 中推荐使用单引号。
(2)内嵌式
写到 script 标签中:


(3)外部式
写到单独的 .js 文件中:



注意:这种情况下 script 标签中间不能写代码,必须空着(写了代码也不会执行)。适合代码多的情况。
3、注释
- 单行注释 //(建议使用)
- 多行注释 /* */
javascript
// 我是单行注释
/*
我是多行注释
我是多行注释
我是多行注释
*/
使用**Ctrl + /**切换注释
多行注释不能嵌套,形如下面这种代码就会报错:
javascript
/*
/*
我是多行注释
我是多行注释
我是多行注释
*/
*/
4、输入输出
(1)输入:prompt
弹出一个输入框:
javascript
// 弹出一个输入框
prompt("请输入您的姓名:");
(2)输出:alert
弹出一个警示对话框,输出结果:
javascript
// 弹出一个输出框
alert("hello");
(3)输出:console.log
在控制台打印一个日志(供程序员看):
javascript
// 向控制台输出日志
console.log("这是一条日志");
注意:在 VSCode 中直接输入 "log" 再按 tab 键,就可以快速输入 console.log。需要打开浏览器的开发者工具(F12)=> Console 标签页,才能看到结果。


这样的输出一般不是给普通用户看的,而是程序员来看的。
A. 重要概念:日志
日志是程序员调试程序的重要手段。
去医院看病,医生会让患者做各种检查,血常规、尿常规、B超、CT 等等。此时得到一堆检测结果。这些结果普通人看不懂,但是医生能看懂,并且医生要通过这些信息来诊断病情。这些检测结果就是医生的 "日志"。
tips:相比于医生来说,程序员多一个终极大招,"重启试试"。
B. 重要概念:.
console 是一个 js 中的 "对象"。
**.**表示取对象中的某个属性或者方法,可以直观理解成 "的"。console.log 就可以理解成:使用 "控制台" 对象 "的" log 方法。
三、语法概览
JavaScript 虽然一些设计理念和 Java 相差甚远,但是在基础语法层面上还是有一些相似之处的。有了 Java 的基础很容易理解 JavaScript 的一些基本语法。
1、变量的使用
(1)基本用法
创建变量(变量定义 / 变量声明 / 变量初始化):

- JavaScript 中还支持使用 let 定义变量,用法和 var 基本类似。let 比 var 出现的晚,意味着避免了 var 这块定义变量的时候的一些缺陷。如果使用 let 定义变量,此时变量的生命周期、作用域基本和 Java 类似。
- JS 中定义一个变量的时候,不需要指定变量的类型,直接赋值即可。
- JS 对于数字只有一种类型,就是数值类型 Number。
- var是 JS 中的关键字,表示这是一个变量。
- = 在 JS 中表示 "赋值",相当于把数据放到内存的盒子中,= 两侧建议有一个空格
- 每个语句最后带有一个 ; 结尾,JS 中可以省略,但是建议还是加上。此处的 ; 为英文分号,JS 中所有的标点都是英文标点。
- 初始化的值如果是字符串 ,那么就要使用单引号或者双引号引起来。
- 初始化的值如果是数字 ,那么直接赋值即可。
使用变量:

为什么动漫中的角色都是要先喊出技能名字再真正释放技能?就是因为变量要先声明才能使用。
弹框提示用户输入信息,再弹框显示:



- 表示字符串拼接,也就是把两个字符串首尾相接变成一个字符串
- \n 表示换行
(2)理解动态类型
A. JS 的变量类型是程序运行过程中才确定的(运行到 = 语句才会确定类型)
javascript
var a = 10; // 数字
var b = "hehe"; // 字符串
B. 随着程序运行,变量的类型可能会发生改变
javascript
var a = 10; // 数字
a = "hehe"; // 字符串
这一点和 C、Java 这种静态类型语言差异较大。C、C++、Java、Go 等语言是静态类型语言,一个变量在创建的时候类型就确定了,不能在运行时发生改变。如果尝试改变,就会直接编译报错。
什么是强类型变量和弱类型变量?强类型变量意味着不同类型之间,变量进行赋值时,需要进行一定的手段(强制类型转化)。
弱类型变量在不同类型之间,变量进行赋值时,可以直接赋值。
什么是动态类型变量和静态类型变量?
动态类型意味着代码在执行过程中,变量类型可以随时发生变化。
静态类型意味着变量定义时是什么类型,在运行过程中就是什么类型。


2、基本数据类型
JS 中内置的几种类型:
- number:数字,不区分整数和小数
- boolean:true 真,false 假
- string:字符串类型
- undefined:只有唯一的值 undefined,表示未定义的值
- null:只有唯一的值 null,表示空值
(1)number 数字类型


JS 中不区分整数和浮点数 ,统一都使用 "数字类型" 来表示。
A. 数字进制表示
计算机中都是使用二进制来表示数据,而我们平时都是使用十进制。因为二进制在使用过程中不太方便(0 1 太多会看花眼),所以在日常使用二进制数字时往往使用八进制 和十六进制来表示二进制数字。
javascript
var a = 07; // 八进制整数, 以 0 开头
var b = 0xa; // 十六进制整数, 以 0x 开头
var c = 0b10; // 二进制整数, 以 0b 开头
注意:
- 一个八进制数字对应三个二进制数字
- 一个十六进制数字对应四个二进制数字(两个十六进制数字就是一个字节)
各种进制之间的转换,不需要手工计算,直接使用计算器即可。
B. 特殊的数字值
- Infinity :无穷大,大于任何数字。表示数字已经超过了 JS 能表示的范围
- -Infinity :负无穷大,小于任何数字。表示数字已经超过了 JS 能表示的范围
- NaN :表示当前的结果不是一个数字


注意:
- 负无穷大和无穷小不是一回事,无穷小指无限趋近与 0,值为 1 / Infinity
- 'hello world' + 1 得到的不是 NaN,而是 'hello world1',会把数字隐式转成字符串,再进行字符串拼接
- 可以使用 isNaN函数判定是不是一个非数字
(2)string 字符串类型
A. 基本规则
字符串字面值需要使用引号引起来,单引号双引号均可。
javascript
var a = "haha";
var b = 'hehe';
var c = hehe; // 运行出错
如果字符串中本来已经包含引号怎么办?
javascript
var msg = "My name is "zhangsan""; // 出错
var msg = "My name is \"zhangsan\""; // 正确, 使用转义字符. \" 来表示字符串内部的引
号.
var msg = "My name is 'zhangsan'"; // 正确, 搭配使用单双引号
var msg = 'My name is "zhangsan"'; // 正确, 搭配使用单双引号
B. 转义字符
有些字符不方便直接输入,于是要通过一些特殊方式来表示:
- \n
- \\
- \'
- \"
- \t
C. 求长度
使用 String 的 length 属性即可。


单位为字符的数量。
D. 字符串拼接
使用 **+**进行拼接:
javascript
var a = "my name is ";
var b = "zhangsan";
console.log(a + b);
数字 和字符串也可以进行拼接:
javascript
var c = "my score is ";
var d = 100;
console.log(c + d);
要认准相加的变量到底是字符串还是数字:


(3)boolean 布尔类型
表示 "真" 和 "假"
- boolean 原本是数学中的概念(布尔代数)
- 在计算机中 boolean 意义重大,往往要搭配条件/循环完成逻辑判断
Boolean 参与运算时当做 1 和 0 来看待。


这样的操作其实是不科学的,实际开发中不应该这么写。
(4)undefined 未定义数据类型
如果一个变量没有被初始化过,结果就是 undefined,是 undefined 类型。


undefined 和字符串进行相加,结果进行字符串拼接:


undefined 和数字进行相加,结果为 NaN:


(5)null 空值类型
null表示当前的变量是一个 "空值"。


注意:
- null 和 undefined 都表示取值非法的情况,但是侧重点不同
- null 表示当前的值为空(相当于有一个空的盒子)
- undefined 表示当前的变量未定义(相当于连盒子都没有)
3、运算符
JavaScript 中的运算符和 Java 用法基本相同。
(1)算术运算符
-
-
- *
- /
- %
(2)赋值运算符 & 复合赋值运算符
-
=
-
+=
-
-=
-
*=
-
/=
-
%
(3)自增自减运算符
- ++:自增1
- --:自减1
(4)比较运算符
- <
- >
- <=
- >=
- == 比较相等(会进行隐式类型转换,比较的是变量的内容)
- !=
- === 比较相等(不会进行隐式类型转换,比较的是内容 + 数据类型)
- !==


(5)逻辑运算符
用于计算多个 boolean 表达式的值
- && 与:一假则假
- || 或:一真则真
- ! 非
(6)位运算
- & 按位与
- | 按位或
- ~ 按位取反
- ^ 按位异或
(7)移位运算
- << 左移
- >> 有符号右移(算术右移)
- >>> 无符号右移(逻辑右移)
4、条件语句
(1)if 语句
A. 基本语法格式
条件表达式为 true,则执行 if 的 { } 中的代码
javascript
// 形式1
if (条件) {
语句
}
// 形式2
if (条件) {
语句1
} else {
语句2
}
// 形式3
if (条件1) {
语句1
} else if (条件2) {
语句2
} else if .... {
语句...
} else {
语句N
}
(2)三元表达式
是 if else 的简化写法。
javascript
条件 ? 表达式 1 : 表达式 2
条件为真,返回表达式 1 的值;条件为假,返回表达式 2 的值。
注意:三元表达式的优先级是比较低的。
(3)switch
更适合多分支的场景。



Debug 模式:


5、循环语句
重复执行某些语句。
(1)while 循环
javascript
while (条件) {
循环体;
}
执行过程:
- 先执行条件语句
- 条件为 true,执行循环体代码
- 条件为 false,直接结束循环
(2)continue
结束这次循环。
吃 5 个李子,发现第 3 个李子里有一只虫子,于是扔掉这个,继续吃下一个李子:
javascript
var i = 1;
while (i <= 5) {
if (i == 3) {
i++;
continue;
}
console.log("我在吃第" + i + "个李子");
i++;
}
(3)break
结束整个循环。
吃 5 个李子,发现第 3 个李子里有半个虫子,于是剩下的也不吃了:
javascript
var i = 1;
while (i <= 5) {
if (i == 3) {
break;
}
console.log("我在吃第" + i + "个李子");
i++;
}
(4)for 循环
javascript
for (表达式 1; 表达式 2; 表达式 3) {
循环体
}
- 表达式 1:用于初始化循环变量
- 表达式 2:循环条件
- 表达式 3:更新循环变量
执行过程:
- 先执行表达式 1,初始化循环变量
- 再执行表达式 2,判定循环条件
- 如果条件为 false,结束循环
- 如果条件为 true,则执行循环体代码
- 执行表达式 3 更新循环变量
6、数组
(1)创建数组
使用 new关键字创建。

注意:Arrary 的 A 要大写。
使用字面量方式创建(常用):

数组中保存的内容称为 "元素"。
注意:JS 的数组不要求元素是相同类型。这一点和 C、C++、Java 等静态类型的语言差别很大,但是 Python、PHP 等动态类型语言也是如此。
(2)获取数组元素
使用下标 的方式访问数组元素(从 0 开始)。


如果下标超出范围读取元素,则结果为 undefined:


相当于本来 arr 是一个数组 ,重新赋值后变成字符串了:


注意:不要给数组名直接赋值,此时数组中的所有元素都没了。
(3)新增数组元素
A. 通过修改 length 新增
相当于在末尾新增元素 ,新增的元素默认值为 undefined


B. 通过下标新增
如果下标超出范围赋值元素 ,则会给指定位置插入新元素


C. 使用 push 进行追加元素
给定一个数组,把数组中的奇数放到一个 newArr 中:


(4)删除数组中的元素
使用 splice方法删除元素。
第一个参数表示从下标为 1 的位置开始删除 ,第二个参数表示要删除的元素个数是 2 个:


目前已经用到了数组中的一些属性和方法。
- arr.length:length 使用的时候不带括号,此时 length 就是一个普通的变量(称为成员变量,也叫属性)
- arr.push()、arr.splice() 使用的时候带括号,并且可以传参数,此时是一个函数(也叫做方法)
7、函数
(1)语法格式
javascript
// 创建函数/函数声明/函数定义
function 函数名(形参列表) {
函数体
return 返回值;
}
// 函数调用
函数名(实参列表) // 不考虑返回值
返回值 = 函数名(实参列表) // 考虑返回值
- 函数定义并不会执行函数体内容,必须要调用才会执行,调用几次就会执行几次
javascript
function hello() {
console.log("hello");
}
// 如果不调用函数, 则没有执行打印语句
hello();
- 调用函数的时候进入函数内部执行,函数结束时回到调用位置继续执行。可以借助调试器来观察。
- 函数的定义和调用的先后顺序没有要求(这一点和变量不同,变量必须先定义再使用)




(2)关于参数个数
实参和形参之间的个数可以不匹配,但是实际开发一般要求形参和实参个数要匹配。
A. 如果实参个数比形参个数多,则多出的参数不参与函数运算


B. 如果实参个数比形参个数少,则此时多出来的形参值为 undefined


JS 的函数传参比较灵活,这一点和其他语言差别较大。事实上这种灵活性往往不是好事。
(3)函数表达式
另外一种函数的定义方式。

此时形如 function() { } 这样的写法定义了一个匿名函数,然后将这个匿名函数用一个变量来表示。后面就可以通过这个 f 变量来调用函数了。
JS 中函数是一等公民,可以用变量保存,也可以作为其他函数的参数或者返回值。
(4)作用域
某个标识符名字在代码中的有效范围。
在 ES6 标准之前, 作用域主要分成两个:
- 全局作用域:在整个 script 标签中,或者单独的 js 文件中生效
- 局部作用域 / 函数作用域:在函数内部生效


创建变量时如果不写 var 或 let ,则得到一个全局变量:


另外,很多语言的局部变量作用域是按照代码块(大括号)来划分的,JS 在 ES6 之前不是这样的。


(5)作用域链
背景:
- 函数可以定义在函数内部
- 内层函数可以访问外层函数的局部变量
内部函数可以访问外部函数的变量,采取的是链式查找 的方式,从内到外依次进行查找。






执行 console.log(num) 的时候,会先在 test2 的局部作用域中查找 num。如果没找到,则继续去 test1 中查找。如果还没找到,就去全局作用域查找。

8、对象
(1)基本概念
对象是指一个具体的事物。
"电脑" 不是对象,而是一个泛指的类别,而 "我的联想笔记本" 就是一个对象。在 JS 中,字符串、数值、数组, 函数都是对象。每个对象中包含若干的属性和方法。
- 属性:事物的特征
- 方法:事物的行为
对象需要保存的属性有多个,虽然数组也能用于保存多个数据,但是不够好。例如表示一个学生信息(姓名小王,身高 175cm,体重 160斤)
var student = '小王', 175, 165;
但是这种情况下到底 175 和 165 谁表示身高,谁表示体重,就容易分不清。
JavaScript 的对象 和 Java 的对象概念上基本一致,只是具体的语法表项形式差别较大。
A. 使用字面量创建对象(常用)
使用 { } 创建对象

- 使用 **{ }**创建对象
- 属性和方法使用键值对的形式来组织
- 键值对之间 使用 **,**分割,最后一个属性后面的 , 可有可无
- 键和值之间 使用 **:**分割
- 方法的值 是一个匿名函数
使用对象的属性和方法:


B. 使用 new Object 创建对象


注意:使用 { } 创建的对象也可以随时使用 student.name = "小王"; 这样的方式来新增属性。
C. 使用构造函数创建对象
前面的创建对象方式只能创建一个对象,而使用构造函数可以很方便的创建多个对象。
例如:创建几个人物对象。
javascript
var xiaowang = {
name: "小王",
height: "175cm",
weight: "170kg",
sayHello: function () {
console.log("Hello");
}
};
var xiaohong = {
name: "小红",
height: "163cm",
weight: "50kg",
sayHello: function () {
console.log("Hey");
}
};
var xiaohuang = {
name: "小黄",
height: "182cm",
weight: "75kg",
sayHello: function () {
console.log("Nice to meet you");
}
};
此时写起来就比较麻烦。使用构造函数可以把相同的属性和方法的创建提取出来,简化开发过程。
基本语法:
javascript
function 构造函数名(形参) {
this.属性 = 值;
this.方法 = function...
}
var obj = new 构造函数名(实参);
注意:
- 在构造函数内部使用 this关键字来表示当前正在构建的对象
- 构造函数的函数名首字母 一般是大写的
- 构造函数的函数名 可以是名词
- 构造函数不需要 return
- 创建对象的时候必须使用 new关键字
this 相当于 "我"
使用构造函数重新创建人物对象:

.
(2)理解 new 关键字
new 的执行过程:
- 先在内存中创建一个空的对象 { }
- this 指向刚才的空对象(将上一步的对象作为 this 的上下文)
- 执行构造函数的代码,给对象创建属性和方法
- 返回这个对象(构造函数本身不需要 return,由 new 代劳了)
(3)JavaScript 的对象和 Java 的对象的区别
A. JavaScript 没有 "类" 的概念
对象 其实就是 "属性" + "方法"
类 相当于把一些具有共性的对象的属性和方法单独提取了出来。
在 JavaScript 中的 "构造函数" 也能起到类似的效果,而且即使不是用构造函数,也可以随时的通过 { } 的方式指定出一些对象。
在 ES6 中也引入了 class关键字,就能按照类似于 Java 的方式创建类和对象了。

B. JavaScript 对象不区分 "属性" 和 "方法"
JavaScript 中的函数是 "一等公民",和普通的变量一样,存储了函数的变量能够通过 ( ) 来进行调用执行。


C. JavaScript 对象没有 private / public 等访问控制机制
对象中的属性都可以被外界随意访问。


D. JavaScript 对象没有 "继承"
继承 本质就是 "让两个对象建立关联",或者说是让一个对象能够重用另一个对象的属性 / 方法。
JavaScript 中使用 "原型" 机制实现类似的效果。
例如:创建一个 cat 对象和 dog 对象,让这两个对象都能使用 animal 对象中的 eat 方法。通过 proto 属性来建立这种关联关系(proto 翻译作 "原型")

当 eat 方法被调用的时候,先在自己的方法列表中寻找。如果找不到,就去找原型中的方法,如果原型中找不到,就去原型的原型中去寻找。最后找到 Object 那里,如果还找不到,那就是未定义了。

下面这段代码在工程语法层面叫 ES6 Class 继承;在底层机制层面,它就是 "通过原型链建立的对象关联(委托)"。



E. JavaScript 没有 "多态"
多态 的本质在于 "程序员不必关注具体的类型,就能使用其中的某个方法"
C++ / Java 等静态类型的语言对于类型的约束和校验比较严格,因此通过子类继承父类,并重写父类的方法的方式来实现多态的效果。
但是在 JavaScript 中本身就支持动态类型,程序员在使用对象的某个方法的时候本身也不需要对对象的类型做出明确区分,因此并不需要在语法层面上支持多态。
在 Java 中,为了让程序员使用方便,ArrayList 和 LinkedList 往往写作 List<String> list = new ArrayList<>(),然后我们可以写一个方法:
java
void add(List<String> list, String s) {
list.add(s);
}
不必关注 list 是 ArrayList 还是 LinkedList,只要是 List 就行。因为 List 内部带有 add 方法,当使用 JavaScript 的代码时:
javascript
function add(list, s) {
list.add(s)
}
add 对于 list 这个参数的类型本身就没有任何限制,只需要 list 这个对象有 add 方法即可,就不必像 Java 那样先继承再重写绕一个圈子。