JavaScript 基础

初识 JavaScript

JS 的作用

1995年,网景公司(Netscape)的员工布兰登·艾奇(Brendan Eich)用 10 天时间就把 Javascript 设计出来了,最初命名为 LiveScript,后来在与 Sun 合作之后将其改名为 JavaScript。

JavaScript 是世界上最流行的语言之一,是一种运行在客户端浏览器的脚本语言(Script 是脚本的意思),常常把 JavaScript 简称为 JS。

JS 编写出来的程序不需要编译就可以直接运行,运行过程中由 JS 解释器(JS 引擎)逐行进行解释并执行。

如今 JavaScript 的使用场景越来越广泛,除了用在浏览器中来和用户进行交互,在其他领域也有广泛的应用。

  • 表单动态校验(密码强度检测) (JS 最初产生的目的)
  • 网页特效
  • 服务端开发(Node.js)
  • 桌面程序(Electron)
  • 移动 App(Cordova)
  • 控制硬件-物联网(Ruff)
  • 游戏开发(cocos2d-js)

浏览器中的 JS

浏览器分成两部分:渲染引擎和 JS 引擎。

  • 渲染引擎:用来解析 HTML 与 CSS,俗称内核,比如新版 Chrome 浏览器的 Blink 内核,Safari 浏览器和早期 Chrome 浏览器所使用的 Webkit 内核。
  • JS 引擎:也称为 JS 解释器,用来读取网页中的JavaScript代码,对其处理后运行,比如 Chrome 浏览器的 V8 引擎。

浏览器内核本身并不会执行 JS 代码,而是通过内置 JavaScript 引擎(解释器)来执行 JS 代码。JS 引擎执行代码时逐行解释每一句源码,将其转换为机器语言,然后交由计算机去执行。因为是因为 JS 是逐行解释执行的,类似于演戏时用到的脚本——演员看了指令就知道自己该表演什么,说什么台词;计算机看了指令就知道自己该做什么事情,所以我们把 JS 归为脚本语言。

JS 的组成

广义上的 JS 的组成:

  • ECMAScript:ECMAScript 规定了 JS 的编程语法和基础核心知识,是所有浏览器厂商共同遵守的一套 JS 语法工业标准。JavaScript(网景公司)和 JScript(微软公司)是 ECMAScript 语言的具体实现,并在此基础做了各自特有的扩展。
  • DOM:文档对象模型(Document Object Model,简称DOM),是 W3C 组织推荐的用来处理可扩展标记语言(XML)的标准编程接口。通过 DOM 提供的接口可以对页面上的各种元素进行操作(大小、位置、颜色等)。
  • BOM:浏览器对象模型(Browser Object Model,简称 BOM),它提供了独立于内容的、可以与浏览器窗口进行互动的对象结构。通过 BOM 可以操作浏览器窗口,比如弹出框、控制浏览器跳转、获取分辨率等。

JS 的位置

JS 有 3 种书写位置,分别为行内、内嵌和外部。

  • 行内式 JS:可以将单行或少量 JS 代码写在 HTML 标签的事件属性值中(大多以 on 开头的属性,如:onclick)。在 HTML 中编写大量 JS 代码时,不方便阅读,并且单双引号多层嵌套匹配时,非常容易弄混,一般只在特殊情况下使用行内式 JS。注意单双引号的使用:在 HTML 中推荐使用双引号将键的值括起来,JS 中推荐使用单引号将字符串括起来。

    1
    <input type="button" value="点我试试" onclick="alert('Hello World');" />
  • 内嵌 JS:可以将多行 JS 代码写到 <script> 标签中,内嵌 JS 是学习 JS 时常用的书写位置,比较方便。

    1
    2
    3
    <script>
    alert('Hello World~!');
    </script>
  • 外部 JS文件:通过 <script> 标签直接引用外部的 JS 文件,适合于 JS 代码量比较大的情况。注意:引用外部 JS 文件的 <script> 开闭标签中间不可以再写任何 JS 代码。

    1
    <script src="my.js"></script>

JS 的输入输出语句

为了方便信息的输入输出,JS 提供了一些输入输出语句与浏览器用户或者开发者进行交互。

  • alert(msg):浏览器弹出警示框,主要用来显示消息给浏览页面的用户。
  • console.log(msg):浏览器控制台打印输出信息,主要用来给程序员自己查看 JS 运行时的消息或者中间结果来更好的调试程序。
  • prompt(info):浏览器弹出输入框,用户可以通过输入框键入一些信息。

变量

变量的使用

变量在使用时分为两步:

  1. 声明变量:var(variable,变量的意思)是一个 JS 关键字,用来声明变量。使用该关键字声明变量后,计算机会自动为变量分配内存空间。

    1
    var age; // 声明一个 名称为 age 的变量
  2. 赋值:把右边的值赋给左边的变量空间中 。

    1
    age = 10; // 给 age 这个变量赋值为 10

也可以在声明一个变量的同时并赋值,我们称之为变量的初始化。JS 引擎在预解析到初始化语句时,会将其拆成声明和赋值两部分,声明置顶,赋值保留在原来位置。

1
var age = 18; // 声明变量同时赋值为 18

同时声明多个变量时,只需要写一个 var, 多个变量名之间使用英文逗号隔开。

1
2
3
var a = 10, b = 20;		// 等同于 var a = 10; var b = 20;
var a = 10
b = 20; // 逗号不能省略,也不能用换行来代替逗号也是不行的,因为 JS 会自动在行末添加分号,等同于 var a = 10; b = 20; b 的作用域可能和上一句有区别。

不要使用 = 号给多个变量同时赋值,可能会造成意料之外的结果。

1
var a = b = c = 10 // 等同于 c = 10;b = c;var a = b; c 和 b 会被当做全局对象的属性。

声明变量特殊情况

只声明一个变量,不赋值就直接使用,变量的值默认为的 undefined

1
2
var age; 
console.log (age); // undefined

没有声明变量,也没有赋值就直接使用,浏览器控制台会报错 xxx is not defined,并且后面的 JS 代码得不到执行,因为 JS 是逐行解释执行的,某一行报错就无法接着执行后面的语句。

1
console.log (age);

不声明变量,直接就给变量赋值,可以正常使用变量,不会报错,但这个变量会被当做隐式全局变量(implicit globals),拥有全局作用域。

1
2
age = 10;
console.log (age);

JS 允许重复声明同一个变量,并且不会导致之前声明的变量的值的丢失,但是如果后一次声明的同时赋值会导致值被覆盖。参考:MDN:Statement - Var

1
2
3
4
5
6
7
var age = 10;
var age;
console.log (age); // 重新声明变量,该变量的值不会丢失,仍然输出 10 而不是 undefined

var age = 10;
var age = 20
console.log (age); // 重新声明并赋值变量,值被覆盖,输出 20

变量命名规范

标识符:就是指开发人员为变量、属性、函数、参数取的名字。标识符不能是关键字或保留字。

关键字:是指编程语言已经规定好某些特殊用途的单词,不能再用它们充当变量名、方法名。 JS 中的关键字包括:break、case、catch、continue、default、delete、do、else、finally、for、function、if、in、instanceof、new、return、switch、this、throw、try、typeof、var、void、while、with 等。

保留字:实际上就是预留的“关键字”,意思是现在虽然还不是关键字,但是未来可能会成为关键字,同样不能使用它们当变量名或方法名。包括:boolean、byte、char、class、const、debugger、double、enum、export、extends、final、float、goto、implements、import、int、interface、long、native、package、private、protected、public、short、static、super、synchronized、throws、transient、volatile 等。
注意:如果将保留字用作变量名或函数名,那么除非将来的浏览器实现了该保留字,否则很可能收不到任何错误消息。当浏览器将其实现后,该单词将被看做关键字,如此将出现关键字错误。

了解上面三个概念后,变量名规范实际上就是标识符规范。

  • 变量名由字母(A-Z、a-z)、数字(0-9)、下划线(_)、美元符号( $ )组成,但是不能以数字开头。如:usrAge,num01, _name,$name,是正确的变量名。可以使用在线工具 JS 变量名验证器判断一个变量名是否合法。
  • 虽然 ECMAScript 规定所有 Unicode letter 均可以作为变量名标识符,但是尽量不要使用中文做标识符。参考:Why aren’t ◎ܫ◎ and ☺ valid JavaScript variable names?
  • 变量名严格区分大小写。var app;var App; 声明了两个不同的变量。
  • 变量名不能是关键字、保留字。例如:var、for、while。
  • 不要使用 name、location、self 作为全局变量名(global variable),这些变量名已经被浏览器的 windows 对象的属性所使用,更多浏览器的属性参考这个链接
  • 变量名建议遵守驼峰命名法。首字母小写,后面单词的首字母需要大写,例如 myFirstName。

数据类型

JavaScript 是一种动态类型语言,不需要提前声明变量的类型,在程序运行过程中,类型会被自动确定,意味着同一个变量在不同时刻还类型可能不一样。参考:弱类型、强类型、动态类型、静态类型语言的区别是什么?

1
2
var x = 6; // x 为数字
var x = "Bill"; // x 为字符串

JS 把数据类型分为两类:

  • 简单数据类型(number,string,boolean,undefined,null),又叫做基本数据类型(primitive data type)或者值类型,在存储时数据的值直接存放在栈空间中,因此叫做值类型。
  • 复杂数据类型(object),又叫做引用类型,引用类型变量在存储时栈空间中存储的仅仅是引用地址,用来指向一个对象实例,真正的对象实例存放在堆空间中,因此叫做引用数据类型。

image-20210101152940197

数字型 number

JavaScript 数字类型既可以用来保存整数值,也可以保存小数(浮点数)。在 JS 中数值字面量前面加 0 表示八进制,字面量前面加 0x 表示十六进制。

1
2
3
4
5
var age = 21; // 整数
var Age = 21.3747; // 小数
var num3 = 012; //八进制字面量, 对应十进制的 10
var num2 = 019; // 虽然以 0 开头,但是八进制中不存在数字 9,所以还是会当成十进制,对应十进制的 19
var num = 0xA; //十六进制字面量,对应十进制的 15

JavaScript 中数值的一些特殊值。

1
2
3
4
5
alert(Number.MAX_VALUE); 	// 最大值,1.7976931348623157e+308
alert(Number.MIN_VALUE); // 最小值,5e-324
alert(Infinity); // 无穷大,大于任何数值,Infinity
alert(-Infinity); // 无穷小,小于任何数值,-Infinity
alert(NaN); // NaN,Not a number,代表一个非数值

可以使用 isNaN() 方法来判断一个变量是否为非数字类型。isNaN(x) 返回 false 说明 x 为数字,返回 true 说明 x 不是数字。

字符串型 string

字符串型的字面量可以是 双引号 "" 和 单引号 '' 的括起来的任意文本,因为 HTML 标签里面的属性使用的是双引号,JS 里我们更推荐使用单引号。当文本中也有引号出现时注意引号的匹配,可以用单引号嵌套双引号 ,或者用双引号嵌套单引号,或者使用下面的转义符号。

类似 HTML 里面的特殊字符,字符串的文本中也有特殊字符,我们称之为转义符。转义符都是反斜杠 \ 开头的,常用的转义符及其说明如下:

转义符 解释说明
\n 换行符,n 是 newline 的意思
\ \ 反斜杠 \
' ‘ 单引号
" ” 双引号
\t tab 缩进
\b 空格 ,b 是 blank 的意思

字符串是由若干字符组成的,这些字符的数量就是字符串的长度。通过字符串的 length 属性可以获取整个字符串的长度。

多个字符串之间可以使用二元操作符 + 进行拼接,其拼接方式为 字符串 + 任何类型 = 拼接之后的新字符串, 拼接前会把与字符串相加的任何类型转成字符串,再拼接成一个新的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
//1.1 字符串 "相加"
alert('hello' + ' ' + 'world'); // hello world

//1.2 数值字符串 "相加"
alert('100' + '100'); // 100100

//1.3 数值字符串 + 数值
alert('11' + 12); // 1112

//1.4 字符串和变量来拼接
var age = 18;
alert('我今年' + age + '岁啦');

布尔型 boolean

布尔类型只有两个值:truefalse。布尔型和数字型相加的时候, true 的值为 1 ,false 的值为 0。

1
2
alert(true + true  + true); // 3
alert(false + 1); // 1

undefined 和 null

一个变量声明后没有被赋值会有一个默认值 undefined,如果和字符串相连拼接,这个变量被视为 ‘undefined’ 字符串,和数字或者布尔型进行数学运算结果会是 NaN

1
2
3
4
5
var variable;
console.log(variable); // undefined
console.log('你好' + variable); // 你好undefined
console.log(11 + variable); // NaN
console.log(true + variable); // NaN

一个变量赋值 null ,即里面存的值为空,表示一个空对象引用,常用来主动释放一个变量引用的对象,表示一个变量不再指向任何对象地址。如果和字符串相连拼接,被视为 ‘null’ 字符串,和数字或者布尔型进行数学运算被当作数值 0 参加运算。

获取变量类型

可以使用 typeof 操作符来检测变量的数据类型,返回一个字符串。

undefinednull 都是只有一个值的特殊类型。typeof 一个没有赋值的变量会返回 undefined,由于某些历史遗留原因,用 typeof 检测 null 返回是 ‘object’,参考Why is typeof null “object”?

image-20210101162608237

数据类型转换

编程中经常要把一种数据类型的变量转换成另外一种数据类型。例如通过 form 表单、prompt() 获取过来的数据默认是字符串类型的,需要转换成数字型,才能进行数学运算。

在浏览器控制台中黑色的输出代表是一串字符,蓝色的代表非字符串。

通常会出现 3 种类型的转换:

  • 转换为字符串类型
    image-20210101164238794

  • 转换为数字型
    image-20210101164301861

  • 转换为布尔型
    image-20210101164809133
    只有代表空、否定的值会被转换为 false ,例如 ''0NaNnullundefined, 其余值都会被转换为 true

    1
    2
    3
    4
    5
    6
    7
    console.log(Boolean('')); 			 // false
    console.log(Boolean(0)); // false
    console.log(Boolean(NaN)); // false
    console.log(Boolean(null)); // false
    console.log(Boolean(undefined)); // false
    console.log(Boolean('false')); // true
    console.log(Boolean(12)); // true

操作符

算术运算符

和其他大多数编程语言一样,也是加减乘除取模五种基本的算术运算符。注意:和 Java 不同,JS 的整数除法结果可以是浮点数,可以使用 parseInt() 给商取整。

image-20210102102731653

和大多数计算机语言一样,浮点数的存储和计算存在精度丢失问题。不要直接判断两个浮点数是否相等 !

1
2
console.log(0.1 + 0.2 == 0.3); // 结果是 false,0.1 + 0.2 结果为 0.30000000000000004
console.log(0.07 * 100); // 结果不是 7, 而是:7.000000000000001

递增和递减运算符

和其他语言一样,也存在前置递增递减和后置递增递减的区别,不再赘述。

比较运算符

比较运算符(关系运算符)是两个数据进行比较时所使用的运算符,比较运算后,会返回一个布尔值(true / false)作为比较运算的结果。

image-20210102103411290

需要注意的是,如果将字符串与数字进行比较,除了 === 运算符 ,JavaScript 会把字符串隐式转换为数值型再进行比较。空字符串将被转换为 0。非数值字符串将被转换 NaN,比较结果始终为 false

比较两个字符串时,从第一个字符往后按字母顺序进行比较。

1
2
3
4
5
6
7
8
console.log(18 == '18');	// true
console.log(18 === '18'); // false
console.log(2 > "John"); // false
console.log(2 < "John"); // false
console.log(2 < "12"); // true
console.log("2" > "12"); // true,当比较两个字符串时,"2" 大于 "12",因为(按照字母排序)1 小于 2。
console.log(undefined == null); // true
console.log(undefined === null); // false

逻辑运算符

逻辑运算符是用来进行布尔值运算的运算符,其返回值也是布尔值。

image-20210102115819197

&&|| 都是短路的。短路:左边的表达式值可以确定逻辑运算的结果时,就不再继续运算右边的表达式。

短路与:表达式1 && 表达式2,如果第一个表达式的值为真,则返回表达式 2,如果第一个表达式的值为假,则返回表达式 1。

短路或:表达式1 || 表达式2,如果第一个表达式的值为真,则返回表达式 1,如果第一个表达式的值为假,则返回表达式 2。

如果表达值式的值不是布尔量会隐式转换为布尔量进行逻辑运算,但是最终返回的是表达式的值本身,而不是表达式所转换得到的布尔量。

1
2
3
4
console.log( 123 && 456 ); // 456,而不是 true
console.log( 0 && 456 ); // 0,而不是 false
console.log( 123 || 456 ); // 123,而不是 true
console.log( 0 || 456 ); // 456,而不是 true

赋值运算法

image-20210102122945155

运算符优先级

image-20210102122958713

流程控制

JS 也有 if-elseswitch-case 两种分支控制语句,forwhile-dodo-while 三种循环控制语句,语法和 C 语言和 Java 一致,不作介绍。唯一需要注意的是 switch 后面跟的表达式可以是字符串或者数值型,与 case 后面的表达式进行的是全等匹配(===)。

数组

创建数组

JS 中创建数组有两种方式:

  • 利用关键字 new 创建数组。

    1
    var arr = new Array(); // 创建一个新的空数组
  • 利用 [] 括起来的数组字面量来创建数组,声明数组的同时并给数组元素赋值。

    1
    2
    var arr = []; 				//使用数组字面量方式创建空的数组
    var strs = ['a','b','c']; //使用数组字面量方式创建带初始值的数组

和 Java 和 C 语言显著不同的是,由于 JS 是动态类型语言,同一个数组中可以存放任意类型的数据,例如字符串,数字,布尔值等。

1
var arr = ['小白',12,true,28.9];

访问数组

可以通过数组索引(下标)来访问数组元素的序号,数组下标从 0 开始。

使用数组的 length 属性可以访问数组元素的数量(数组长度)。配合 for 循环可以索引遍历数组中的每一项。

1
2
3
4
var arr = ['red','green', 'blue']; 		//arr.length 为3
for(var i = 0; i < arr.length; i++){
console.log(arrStus[i]);
}

length 属性是可读写的,当我们数组里面的元素个数发生了变化,这个 length 属性自动跟着一起变化。

可以通过修改 length 长度来给数组扩容,扩容产生的新增元素默认值是 undefined

1
2
3
4
5
var arr = [0, 1, 2]
arr.length = 5;
console.log(arr); // [ 0, 1, 2, <2 empty items> ]
console.log(arr[4]); // undefined
console.log(arr); // [ 0, 1, 2, <2 empty items> ]

也可以通过索引范围外的数组元素赋值来扩容数组,JS 中不存在索引越界错误。

1
2
3
4
5
6
var arr = [0, 1, 2]
console.log(arr); //[ 0, 1, 2 ]
console.log(arr[4]); //undefined
console.log(arr); //[ 0, 1, 2 ],直接越界访问不报错,但是不会扩容数组
arr[4] = 4;
console.log(arr); //[ 0, 1, 2, <1 empty item>, 4 ]

使用 length 属性作数组索引给元素数组元素赋值,可以实现数组动态增长。

1
2
3
4
var arr = [];
for (var i = 0; i < 100; i ++) {
arr[arr.length] = i;
}

函数

声明函数

函数的两种声明方式:

  • 自定义函数方式(命名函数):利用函数关键字 function 声明函数。因为预解析的存在,调用函数的代码既可以放到声明函数的前面,也可以放在声明函数的后面。由于函数一般是为了实现某个功能才定义的, 所以通常我们将函数名命名为动词,比如 getSum,同样函数名也属于标识符,要遵循前面提到的标识符规范。

    1
    2
    3
    4
    function fn({
        ... //函数体代码
    }
    fn(); //通过函数名调用,调用函数的代码既可以放到声明函数的前面,也可以放在声明函数的后面
  • 函数表达式方式(匿名函数):声明一个变量,变量里面存储的是一个函数对象,函数本身是没有名字的(匿名),只能通过变量名来调用函数,函数调用的代码必须写到函数体后面。

    1
    2
    3
    4
    var fn = function(){
    ...
    }; // 这是函数表达式写法,匿名函数后面跟分号结束
    fn(); // 通过变量名调用函数,函数调用必须写到函数体下面

调用函数

声明函数本身并不会执行代码,只有调用函数时才会执行函数体代码。

JS 中不会根据实参和形参列表的匹配情况进行函数重载,可能存在函数形参和实参个数不匹配的问题。
image-20210103142023058

1
2
3
4
5
6
function sum(num1, num2{
    console.log(num1 + num2);
}
sum(100200);             // 形参和实参个数相等,输出正确结果
sum(100400500700);   // 实参个数多于形参,只取到形参的个数
sum(200);                  // 实参个数少于形参,形参的默认值是 undefined,结果为 NaN

执行到函数体中的 return 语句时,函数会停止执行,并返回指定的值。如果函数没有 return 语句,执行完整个函数体后,返回的值是 undefinedreturn 只能返回一个值。如果用逗号隔开多个返回值,则实际上一个逗号表达式,整个逗号表达式的值是最后一个表达式的值。但在函数调用时,各个实参之间是用逗号隔开的,参数列表中的逗号就不是逗号运算符。

当我们不确定有多少个参数传递的时候,可以用 arguments 对象来获取。在 JavaScript 中,arguments 是当前函数的一个内置对象。所有函数都内置了一个 arguments 对象,arguments 对象中存储了传递的所有实参。arguments 展示形式是一个伪数组,可以通过数组索引获取第几个参数,也可以配合 length 属性来遍历所有实参,但是不能使用普通数组中的 push() , pop() 等方法。

函数的形参也可以看做是一个变量,当把一个值类型变量作为参数传给函数的形参时,其实是把变量在栈空间里的值复制了一份给形参,那么在方法内部对形参做任何修改,都不会影响到的外部变量。当把引用类型变量传给形参时,其实是把变量在栈空间里保存的引用地址复制给了形参,形参和实参指向的是同一个堆地址,所以操作的是同一个对象实例。

作用域

JavaScript 在 ES 6 标准前只有以下两种作用域,没有块级作用域:

  • 全局作用域:作用于所有代码执行的环境(整个 <script> 标签内部)或者一个独立的 JS 文件。
  • 局部作用域:作用于函数内的代码环境,就是局部作用域。 因为跟函数有关系,所以也称为函数作用域。

在 JavaScript 中,根据作用域的不同,变量可以分为两种:

  • 全局变量:在函数外部通过 var 声明的变量或者任意位置不用 var 声明就直接使用的变量,后一种是隐式全局变量(implicit globals),实际上是 window 对象的一个属性,可以使用 delete 操作符删除这些属性,虽然也全局作用域可见,但是和真正的全局变量有一点差异,一般不建议使用。全局变量拥有全局作用域,在任何一个地方都可以使用,只有在浏览器标签页面关闭时才会被销毁,因此比较占内存。
  • 局部变量:在函数内部通过 var 声明的变量,函数的形参实际上也是局部变量。局部变量拥有局部作用域,只能在该函数内部使用,当函数运行结束后,就会被销毁,因此更节省内存空间。

作用域链

如果函数声明中还有另外的函数声明,那么在这个局部作用域中就又诞生一个局部作用域,根据在内部函数可以访问外部函数变量的这种机制,用链式查找决定哪些数据能被内部函数访问,这种机制就称为作用域链。
示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
function f1() {
var i = 2; // 在函数 f1() 局部作用域声明一个新的局部变量 i 并初始化,不影响全局变量 i
function f2() {
i = 3; // f2() 函数内部可以访问外部函数的局部变量
console.log('i=' + i); //i=3
}
f2();
console.log('i=' + i); // i=3
}
var i = 1;
f1();
console.log('i=' + i); //i=1

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
function f1() {
i = 2; // f1() 可以访问修改全局变量 i
function f2() {
var i = 3; // f2() 函数内部声明一个新的局部变量 i 并初始化
console.log('i=' + i); //i=3
}
f2();
console.log('i=' + i); // i=2
}
var i = 1;
f1();
console.log('i=' + i); //i=2

示例 3:

1
2
3
4
5
6
7
8
9
10
function f1() {
var a = b = c = 9; // 等同于 c = 9;b = c;var a = b; c 和 b 会被当做全局变量
console.log(a); // 9
console.log(b); // 9
console.log(c); // 9
}
f1();
console.log(c); // 9
console.log(b); // 9
console.log(a); // ReferenceError: a is not defined

预解析

JavaScript 代码是由浏览器中的 JavaScript 解析器来执行的。JavaScript 解析器在运行 JavaScript 代码的时候分为两步:预解析和代码执行。

  • 预解析:在当前作用域下,JS 代码执行之前,浏览器会默认把通过 var 声明的变量和通过 function 声明的函数在内存中进行提前声明。
  • 代码执行: 从上到下顺序解释执行 JS 语句。

建议始终在作用域顶部声明变量(全局代码的顶部和函数体代码的顶部),这可以清楚知道哪些变量是全局作用域,哪些变量是局部作用域。

变量预解析

变量提升(Hoisting): 变量的 var 声明语句会被提升到当前作用域的最上面,变量的赋值语句不会提升。

1
2
3
4
5
6
7
8
9
function fn(){
console.log(num);
var num = 20;
console.log(num);
}
console.log(num);
var num = 10;
console.log(num);
fn();

经过预解析后等效为:

1
2
3
4
5
6
7
8
9
10
11
var num;
function fn(){
var num;
console.log(num); // undefined
num = 20;
console.log(num); // 20
}
console.log(num); // undefined
num = 10;
console.log(num); // 10
fn();

函数预解析

函数提升: 命名函数的 function 声明语句会被提升到当前作用域的最上面,因此在函数声明之前就可以调用函数。

1
2
3
4
fn();
function fn() {
console.log('halo');
}

经过预解析后等效为:

1
2
3
4
function fn() {
console.log('halo');
}
fn();

匿名函数的声明是通过声明变量实现的,进行的实际上是变量提升,因此在没有让变量真正地引用一个函数对象之前,不能调用函数。

1
2
3
4
fn();
var fn = function() {
console.log('halo');
}

经过预解析后等效为:

1
2
3
4
5
var fn;
fn(); // TypeError: fn is not a function
fn = function() {
console.log('halo');
}

对象

创建对象

在 JavaScript 中,现阶段我们可以采用三种方式创建对象(object):

  • 利用字面量创建对象,可以用花括号 {} 括起来的键值对的形式表示对象,冒号前面的键是对象的属性名或者方法名,冒号后面的值是属性值或者方法体。

    1
    2
    3
    4
    5
    6
    7
    8
    var star = {
    name : 'pink',
    age : 18,
    sex : '男',
    sayHi : function(){
    alert('大家好啊~');
    }
    };
  • 利用 new Object() 创建对象,然后给对象不存在的属性赋值,就相当于给对象动态地添加属性或者方法。

    1
    2
    3
    4
    5
    6
    7
    var andy = new Obect();
    andy.name = 'pink';
    andy.age = 18;
    andy.sex = '男';
    andy.sayHi = function(){
    alert('大家好啊~');
    }
  • 利用构造函数创建对象:构造函数是一种特殊的函数,主要用来创建某一类对象并初始化,即为对象成员变量赋初始值,它总与 new 操作符一起使用。我们可以把对象中一些公共的属性和方法抽取出来,然后封装到这个函数里面。构造函数的首字母通常要大写,函数内的属性和方法前面需要添加 this 关键字,this 指向正在创建的对象,构造函数里面的代码可以给这个新对象添加属性和方法。使用 new 调用构造函数会自动返回所创建的新对象,不需要使用 return 语句返回。构造函数抽象了对象的公共部分,封装到了函数里面,可以用来泛指某一大类(class),通过 new 关键字创建对象的过程我们也称为对象实例化。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Person(name, age, sex) {
    this.name = name;
    this.age = age;
    this.sex = sex;
    this.sayHi = function() {
    alert('我的名字叫:' + this.name + ',年?:' + this.age + ',性?:' + this.sex);
    }
    }
    var bigbai = new Person('大白', 100, '男');
    var smallbai = new Person('小白', 21, '男');
    console.log(bigbai.name);
    console.log(smallbai.name);

使用对象

可以通过 . 或者 [] 来访问对象里面的属性,方括号里面只能是字符串,属性名必须先用引号括起来成字符串常量。可以通过 对象.方法名() 来调用对象的方法,方法名字后面一定加括号。

1
2
3
console.log(star.name) 		
console.log(star['name'])
star.sayHi();

for-in 语句用于对对象的所有属性进行操作,语法中的变量名可以是任何符合命名规范的标识符 ,通常我们会将这个变量写为 k 或者 key。for-in 语句也可以用来遍历数组中的所有元素,这时候 k 代表数组索引。

1
2
3
4
for (var k in obj) {
console.log(k); // 这里的 k 是属性名
console.log(obj[k]); // 这里的 obj[k] 是属性值
}

JS 内置对象

JavaScript 中的对象分为 3 种:自定义对象、内置对象、浏览器对象,前面两种对象属于 ECMAScript 规范,浏览器对象属于 JS 独有。内置对象就是指 JS 语言自带的一些对象,这些对象提供了一些常用的功能(属性和方法),帮助开发者快速开发。

Math

1
2
3
4
5
6
7
8
Math.PI		 			// 圆周率
Math.floor() // 向下取整
Math.ceil() // 向上取整
Math.round() // 四舍五入版 就近取整 注意 -3.5 结果是 -3
Math.abs() // 绝对值
Math.max()/Math.min() // 求最大和最小值
Math.random() // 方法可以随机返回一个小数,其取值范围是 [0,1)
Math.floor(Math.random() * (max - min + 1)) + min // 得到一个两个数(max,min)之间的随机整数,包括两个数在内

Date

image-20210107192415227

注意:getMonth()getDay() 返回的值是以 0 开始计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
var time = new Date();
var year = time.getFullYear();
var month = time.getMonth() + 1;
var dates = time.getDate();
var arr = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'];
var day = time.getDay();
var h = time.getHours();
h = h < 10 ? '0' + h : h; // 给时分秒补 0
var m = time.getMinutes();
m = m < 10 ? '0' + m : m; // 给时分秒补 0
var s = time.getSeconds();
s = s < 10 ? '0' + s : s; // 给时分秒补 0
var date = year + '年' + month + '月' + dates + '日' + arr[day] + h + ':' + m + ':' + s;

Date 对象需要实例化后才能使用,如果调用构造函数 Date() 不写参数,就返回执行到 new 语句时的当前的系统时间,如果 Date() 里面写参数,就返回参数里面的时间。例如日期格式字符串为 ‘2019-5-1’,可以写成 new Date('2019-5-1') 或者 new Date('2019/5/1')

1
2
var now = new Date();				
console.log(now.getFullYear());

Date 对象中存储了某一时刻距离 1970年1月1日(世界标准时间)起的毫秒数,经常利用这个数值来进行天、时、分、秒的换算。可以利用 valueOf()getTime() 方法来得到这个数值,也可以使用一元操作符 +

1
2
3
4
5
var now = new Date();			
console.log(now.valueOf())
console.log(now.getTime())
var now = + new Date();
var now = Date.now(); // HTML5 中提供的方法,有兼容性问题

Array

1
2
3
4
5
6
var arr = [1, 23];
var obj = {};
console.log(arr instanceof Array); // true ,instanceof 运算符,可以判断一个对象是否属于某种类型
console.log(obj instanceof Array); // false
console.log(Array.isArray(arr)); // true, Array.isArray()用于判断一个对象是否为数组,isArray() 是 ES6 中提供的新方法
console.log(Array.isArray(obj)); // false

下面四个方法用来添加删除数组元素。

image-20210107193544194

reverse()sort() 对整个数组元素反转和排序。

image-20210107193902874

sort() 需要传入一个比较策略,根据传入的函数对象来排序。

1
2
3
4
5
var arr = [1, 64, 9, 6];
arr.sort(function(a, b) {
return b - a; // 降序
// return a - b; // 升序
});

indexOf()lastIndexOf() 用来查找某一个元素元素在数组中的位置。

image-20210107193732756

toString()join() 用来把数组转换为字符串。

image-20210107194359554

concat()slice()splice() 用来连接数组和删除数组。

image-20210107194417328

String

不可变性:字符串对象的值不可变,虽然看上去可以给字符串对象重新赋值,但其实是引用对象指向了内存中新开辟的空间,这个空间中存放了新的字符串对象。由于字符串的不可变,字符串所有的方法,都不会修改字符串本身,而是返回一个新的字符串,因此在大量拼接字符串的时候会有效率问题。可以先将需要拼接的字符串依次传入一个数组,然后使用 array.join('') 把整个数组的字符串拼接,可以大大提高效率。

indexOf()lastIndexOf() 方法用于查找字符串中的字符出现位置。

image-20210107200320304

charAt()charCodeAt()str[] 根据索引返回字符。

image-20210107200723031

concat()substr()slice()substring() 方法用于字符串连接和截取。

image-20210107200932002

replace() 方法可以进行简单的字符串替换,支持使用正则表达式进行匹配。

1
2
var p = 'Apples are round, and apples are juicy';
console.log(p.replace('apple', 'orange')); //在字符串中用一些字符替换另一些字符,Apples are round, and oranges are juicy

split() 方法用于切分字符串,它可以将字符串切分为字符串数组。

1
2
var str = 'a-b-cc-dd-bbb';
console.log(str.split(',')); // 返回的是一个数组 [ 'a', 'b', 'cc', 'dd', 'bbb' ]

包装类型

为了方便操作基本数据类型,JS 提供了基本类型的包装类型,用来把简单数据类型包装成为复杂数据类型,这样基本数据类型就有了属性和方法。除了 nullundefined ,其他的基本类型都有对应的包装类型。例如 string,number,boolean,对应了StringNumberBoolean三种基本包装类型。

1
2
3
var str = 'hello';
var len = str.length;
console.log(len);

按道理基本数据类型是没有属性和方法的,对象才有属性和方法,但上面的代码却不会报错,这是因为 JS 会把基本数据类型变量自动包装成复杂数据类型:先创建临时的包装类型对象,然后再访问属性或者方法,其执行过程如下 :

1
2
3
4
5
var str = 'hello';				// typeof str === 'string', str instanceof String == flase 
var temp = new String(str); // 1.创建 String 类型的一个临时对象,typeof temp === 'object',temp instanceof String == true
len = temp.length; // 2 在实例上访问方法或者属性
temp = null; // 3.销毁这个临时实例
console.log(len);

访问完属性或者方法之后,就立即销毁该对象,这就意味着在运行时为基本类型值添加属性和方法是没有意义的。

1
2
3
var str = 'hello';
str.sex = 'male'; // 创建了一个基本包装类型的临时对象,然后给这个对象的 sex 属性赋值为 male,这条语句结束,刚才创建的临时对象被销毁
console.log(str.sex); // undefined,试图获取 str.sex 的值时,又会创建一个基本包装类型的对象,但是新创建的对象没有 sex 属性

参考

  1. B 站视频:JavaScript 基础语法
  2. 阮一峰:Javascript 诞生记