JavaScript基础

99 阅读16分钟

诞生

1、早期的服务端语言表单校验、用户交互困难

2、与java没有关系,仅仅为了蹭热度

JavaScript实现组成

1、核心语言(ECMAScript)

2、文档对象(DOM)

3、浏览器(BOM)

浏览器只是ECMAScript实现的宿主环境

DOM

DOM将页面映射为树结构,提供给开发人员控制页面内容和结构的能力。

DOM1级:映射文档结构,如document等对象API

DOM2级:鼠标及键盘事件处理接口、样式接口、遍历和操作文档树的接口

BOM

提供操作浏览器的对象模型

1、移动、缩放、打开、关闭窗口及window对象

2、浏览器详细信息的navigator对象

3、窗口加载的页面详细信息location对象

4、显示器分辨率信息screen

5、cookies

6、XMLHttpRequest和IE的ActiveObject等自定义对象

BOM的实现根据浏览器不同而存在差异,HTML5的出现让BOM的实现朝着兼容性越来越高的方向发展

脚本嵌入-script标签

1、脚本默认是阻塞串行运行的,并且会阻塞DOM渲染(浏览器遇到body才开始呈现内容),一般的处理方式是将脚本放在底部(尾部body之前),避免白屏

2、defer属性,规定多个defer按顺序执行脚本,且在文档渲染完成,DOMContentLoaded事件前执行

3、async属性的脚本将并行请求及执行。async脚本谁先加载完成谁先执行,因此async脚本互不依赖很重要

4、推荐使用外部脚本文件,具有更好的可维护性,且可缓存(同一个脚本文件被多个页面公用只需要下载一次)

defer与async性能优化

明确一点: js的执行不可能和DOM同步进行,因为js拥有修改DOM的权利。但下载可以

1、defer规定多个defer按顺序执行脚本,实际情况是,多个defer并不一定会按顺序执行,也不一定在 DOMContentLoaded事件前。因此最好只使用一个defer脚本

2、正常脚本执行流程:所有脚本串行下载执行,每个脚本下载并执行完成才处理下一个脚本;脚本处理完成后开始解析html(浏览器遇到body开始呈现)

2、defer脚本执行流程:html解析与defer脚本并行下载,两者均完成后执行defer脚本

3、async脚本执行流程:html解析与async脚本并行下载,如果async脚本下载完成,立即执行(阻塞html解析),多个async脚本

4、defer使用:如果脚本依赖于DOM或者脚本作为依赖或补丁注入(被依赖)使用defer脚本;

5、defer最主要的场景是依赖注入补丁文件polyfill.js、依赖DOM如按钮点击事件

6、async的脚本是谁先下载完成谁先执行,无法处理依赖,且执行期间会阻塞html解析,如果你的不关心DOM,且不存在文件依赖,可以使用async脚本。如百度统计、集成的第三方聊天、评论服务

7、正常脚本与异步脚本如何加载?

doctype文档声明

1、规定了文档以哪种方式渲染html

2、如果没有声明,每个浏览器按自己的标准解析,称为混杂模式

3、w3c标准模式,使用html5的规范解析,提高兼容性

关键字

变量、语句-条件分支-循环及中断-异常捕获-类型捕获

var function delete new this debugger void finally with

if else do while for in switch case default break return continue

try catch throw instanceof typeof

其他的一些语法词都是保留字

8大数据类型

基础数据类型:String、Number、Boolean、Undefined、Null及ES6的Symbol、BigInt

复杂类型:Object

typeof

typeof(Number)等数据类型,检验的是数据类型的构造函数,返回function

typeof的数据返回

1、未声明或声明未初始化 =>"undefined"

2、字符串 => "string"

3、数字 => "number"

4、函数、正则表达式 => "function"

5、Symbol类型数据 => "symbol"

6、Boolean类型数据 => "boolean"

7、null、数组、{}、普通对象等 => "object"

8、函数 => "function"

函数是一组独立的功能集合,因此把它和object区分出来

typeof null 返回object。因为null存储的是一个空指针引用,因此如果变量将存储对象,可以声明为null或{}

实际上,undefined派生自null

typeof只能用来检测基本数据类型,检测复杂类型使用instanceof,通用检测使用Object.prototype.toString.call(a)

检测数组isArray

注意:如果变量未定义,直接条件判断将报错,而使用typeof检测确能返回undefined

Undefined类型

Undefined类型只有一个值undefined,指声明但未进行初始化的变量,引入这个值是为了正式区分空对象指针与未经初始化的变量

返回undefined的情况,一般是程序解析给的默认值

1、声明但未初始化

2、数组中,某一项没有值,值是undefined

3、形参接收不到值,接收到的就是undefined

4、函数没有返回值,相当于返回了undefined

5、对象没有这个属性,非要获取这个属性的值,这个属性的值也是undefined

undefined派生于null,引入undefined的目的就是为了和null区分,null一般是主动指定的,undefined则是程序解析得到

Null类型

Null类型只有一个值,那就是null,null表示一个空对象指针,如果定义的变量将用来存储对象,那么最好将它初始化为null(而不是{})

null与{}的区别

1、{}不是一个完全空的对象,原型链上有Object

2、null是原型链的顶端

Object.prototype.__proto__ === null返回true,没有原型链

3、在转化为布尔值时,对于对象而言,只有null会转化为false,其他的[]或{}都会转化为true

Boolean类型

布尔类型只有true和false两个字面量,他们与1和0数字值不是一个概念。在控制流语句(如if)中,会将其他类型的变量转化为布尔类型

其他类型转化为布尔类型

1、undefined转化为false

2、对于数字,只有0和NaN转化为false,其他数字转化为true

3、对于字符串,只有空字符""转化为false,其他非空字符串如"0"转化为true

4、对于对象,只有null转化为false,其他对象包括{}、[]转化为true

Number类型

js的数字是以64位双精度浮点数的形式存储,格式为(s) * (m) * (2^e),s 是符号位,表示正负。m 是尾数,有 52 bits。e 是指数,有 11 bits,e 的范围是 [-1074, 971]

因此Number.MAX_VALUE的值为1 * (Math.pow(2, 53) - 1) * Math.pow(2, 971);Number.MIN_VALUE的值为1 * 1 * Math.pow(2, -1074)

十进制转二进制:

1、整数部分 "除二取余,倒序排列",小数部分 "乘二取整,顺序排列"

2、小数部分转化为2进制:用2X小数,积的整数部分记录,小数部分如果不为0继续X2获取积,直到积的小数部分为0或者达到最大尾数(53位对应到10进制就是17位小数)

3、因为上述原因导致了0.1 + 0.2 === 0.3返回false

科学计数法:使用1.2e-7这种形式表示 代表的涵义为1.2乘以10的-7次方。对于极大或极小数可以精简代码

NaN: 与任意数操作均返回NaN,且NaN不等于NaN本身。唯一的检测手段是isNaN函数

常用属性和方法:

1、Number.NaN、Number.MAX_VALUE、正负无穷大、Number.MIN_VALUE

2、toFixed(2)保留2位小数

3、toString(2)将数字转化为2进制的字符串

4、toPrecision(2)转化为2位科学计数法表示

5、toExponential返回指数表示法

其他类型转Number

Number(a)的转化规则

1、null,返回0

2、undefined,返回NaN

3、不可转化的字符串将返回NaN,可以转化的字符串将被转化为对应的整数或浮点数。其他进制的字符串将被转化为10进制

4、对象,先调用toString转化为字符串(Object的toString返回"[object Object]",而数组则是扁平化),然后转化为数字

parseInt:解析所有数字符号,如果第一个字符不是数字或者正负号,将返回NaN(注意,小数点也不是有效的数字字符)

parseInt("a123") // 返回NaN

parseInt("a123", 16) // 41251进制转化

parserInt("123ab") // 123

parseInt("12.3") // 1

parseInt("07") // 7

parseInt进制转化,第一个参数为字符串,第二个参数代表参数的进制数(默认为10进制);返回转化的10进制数

parseFloat的区别

1、能解析第一个小数点(不能解析第二个)

2、parseFloat没有第二个参数,会忽略前置的0,并对16进制始终返回0

parseFloat("1.23") // 1.23

parseFloat("1.23.4") // 1.23

parseFloat("071") // 71

数据转化

1、显式转化,调用Number、Boolean、parseInt等进行转化

2、隐式转化,程序自己转化

隐式转化:

1、四则运算,特别注意加法,一个是字符串都将转化为字符串返回String;而加法之外的其他运算,一个是数字,都将转化为Number

2、特殊符号,如~等,转化为Number

3、判断语句中的表达式被转化为Boolean

4、宿主原生代码调用,如alert,接受String类型,如果是对象会调用toString返回object,而数组则会返回扁平化的字符串

String类型

字符字面量:也称为转义字符,用于html中正确解析为字符串

字符串的特点-不可变:填充新的字符串销毁老的

let str = "Java"; str = str + "Script"

先创建10个字符的字符串,然后销毁原来的字符串"Java"和"Script"。这是早期字符串拼接慢的原因(现在浏览器已经解决)

转化为字符串:除了null和undefined,基本所有的数据都有toString方法用来转化为字符串

toString可以接受一个参数,转化的进制

let num = 10; num.toString(2); // 将10转化为2机制,得到1010

Object类型

Object

js的对象实质是一组数据和功能的集合,Object具有的所有属性和方法都存在于更具体的对象中

实例共享:

1、构造函数constructor,保存着用于创建对象的函数

2、hasOwnProperty,用于检查给定的属性在当前对象实例中(不包括原型)

3、 parent.isPrototypeOf(child):用于检查传入的对象是否是传入对象的原型

4、propertyIsEnumerable(propertyName) :用于检查给定的属性是否能够使用 for-in 语句 (本章后面将会讨论)来枚举。

5、toString():返回对象的字符串表示"[object, Object]"或者重写的的return值

6、valueOf():返回对象的字符串、数值或布尔值表示。通常与 toString() 方法的返回值 相同。

注意toString方法,可以通过原始Objec的toString来检测数据类型Object.property.toString.call() 而不能直接调用toString,因为派生的如Array等都重写了toString方法,只能得到重写后的结果,而达不到检测数据类型的效果

操作符

针对对象的转化

先调用对象的valueOf方法获得对象的表示值,如果没有valueOf方法,将调用toString方法转化为字符串,如果需要转化为数值,再对字符串Number强转

js操作符包括算术操作符(如加号和减号)、位操作符、关系操作符和相等操作符。js操作符适用如字符串、数字值、布尔值,甚至对象。 不过,在应用于对象时,相应的操作符通常都会调用对象的valueOf()或toString()方法,以便取得可以操作的值

前置与后置递增(减)

1、对于前置操作,变量的值是在求值之前改变的

let a = 20; let b = --a + 1; console.log(a); console.log(b); // 输出19、20,因为a先改变再参与求值

2、对于后置操作,变量在求值之后改变

let a = 20; let b = a-- + 1; console.log(a); console.log(b); // 输出19、21,先求值后改变a

隐式转化:+、-、*、~等

用于隐式转化变量,相当于调用Number(a),将其转化为数字或数字的负数(-)

布尔操作符

1、逻辑非!,隐式转化,相当于Boolean(a)的取非值。我们可以通过!!a来隐式的将a转化为布尔类型(效果等同显式转化Boolean(a))

2、逻辑与&&,必须两者同时为真才返回真。

可以用来简化单个分支语句,if (a) { console.log(1); }可以简化为a && console.log(1);

3、逻辑或||,任一为真就返回真

常用来处理为null或undefined数据设置默认值。let a = obj.a || '默认'; (ES6可以通过解构赋值的形式设置默认)

比如使用后台返回数据的某个字段,如果返回的数据为空,那么拿不到属性而报错

if (res.names) {
    res.names.map(..);
}

可以简化为result.name && result.names.map(item => item.name)

4、注意,上述为了避免null和undefined情况,特别注意0的情况,0 || "默认值"将返回默认值,解构赋值则可以避免这个问题(解构赋值使用的是iterator)

*乘法

1、一个操作数为NaN返回NaN

2、无穷大与0相乘,返回NaN;与非0数(包括无穷大)相乘返回无穷大

3、不是数值,将调用Number(a)转化后运算

let arr1 = [1];
let arr2 = [1,2];
let obj = { id: 1 };

arr1*1; // 返回1
arr2*1; // 返回NaN,先转化为字符串"1,2",然后转化为数字NaN
obj*1; // 返回NaN

/除法

1、一个操作数为NaN返回NaN

2、无穷大除以无穷大,返回NaN

3、0除以0返回NaN,无穷大除以0,返回无穷大,有限数除以0返回无穷大

4、不是数值,将调用Number(a)转化后运算

%求模(余数)

规则相当于是做除法

+加法

1、一个操作数为NaN返回NaN

2、正无穷与负无穷相加返回NaN。同正同负返回本身

3、一个操作数是字符串,转为字符串拼接(因此要特别注意加法运算,考虑字符串的情况,可以通过Number显式转化,或-0、~等隐式转化)

-减法

1、一个操作数为NaN返回NaN

2、正无穷减正无穷返回NaN,减其他任意数(包括负无穷)均返回正无穷;负无穷减负无穷返回NaN,减其他任意数(包括正无穷)均返回负无穷

3、不是数字,将调用Number(a)转化后运算

关系操作符

关系操作符包括大于(>)、小于(<)、大于等于(>=)、小于等于(<=)

1、两个操作数都是字符串,比较ASCII编码(区分大小写).

'13' < '3';返回true,比较的是ASCII码

2、一个为数字,另一个不是数字,转化为数字。

'13' < 3;返回false,先转化为Number

3、NaN与任意数值比较均返回false(既不大于也不小于和等于)

'a' > 3; 返回false,'a'转化为Number是NaN,NaN与任意值比较返回false

相等操作符

包括相等( == )、不等(!=)、恒等( === )、不恒等( !== )

对于相等与不等比较

1、类型相同,基本类型,比较值是否相同;对象,比较引用地址是否一致

{ id: 1 } == { id: 1 }返回false,他们引用不同,不是一个对象

2、字符串与数值比较,转化为Number

3、对象与基础类型比较,对象遵循上述转化规则去比较

let a = {}; a == 0;返回false,先valueOf返回{},不等于0

4、null与undefined相等,注意null与undefined在比较相等时不做任何转化,即undefined、null与0、""、false等比较均返回false

5、NaN与任何数据比较相等返回false,比较不等返回true

对于全等于不全等,不仅需要比较数据相等,还需要数据类型保持一致,为了保持代码中数据类型的完整性,我们推荐使用全等和不全等操作符。

undefined == null返回true;但undefined === null返回false,因为类型不同

三元表达式

expression ? trueValue : falseValue

常用来优化条件分支,可以让代码更简洁

let a = [1];
let b = 0;

if (a.length) {
    b = 1;
} else {
    b = 2;
}

// 使用三元表达式优化
a.length ? b = 1 : b = 2;

总结

1、比较相等,null与undefined相等,但null与undefined不做转化,他们与0相比均不相等

2、对于字符串与数字相等,将转化为数字然后比较,因此0与‘0’比较相等

3、对于条件分支if中的语句,将转化为布尔值,因此通过if(a)的形式,可以排除undefined、null、0、false等情况;但对于'0'、{}、'false'等都将返回true

4、特别注意加法,数字与字符串相加将转化为字符串计算

5、调用toFixed之前一定要注意操作数必须是一个数字,操作前将数据转化为Number,不然将报错

6、尽量使用Number、String、Boolean显式转化,更语义化。但在计算时,可以通过隐式转化来处理

严格模式

为JavaScript容易出错的地方添加了限制

语句

条件分支及循环

1、条件分支if,即使代码只有一句,也不要省略代码块{},这样容易让修改发生错误。如果分支简单,使用&&或三元表达式进行优化

2、while与do-while的区别是do-while语句至少会执行一次,while可能一次都不执行

3、for循环,要么使用倒序for循环(只需要获取一次len),要么缓存len

4、for-in用来枚举对象属性(不包括继承的属性和Symbol),注意,for-in循环的属性没有顺序。如果for-in的是一个null或undefined数据,早期是会抛出错误的,ES6修正了这个问题(不再执行循环),但为了最大兼容性,for-in之前先检测null与undefined

5、循环可以省略var声明i(相当于声明了全局变量),为了保持局部变量,不要省略

6、for循环永远是性能最好的,但==为了可维护性与简洁性,尽量使用ES6自带的处理函数(如map、filter、reduce、find)==

7、更清晰的条件分支switch,结构更清晰,推荐使用switch使用的是全等比较,不会出现类型转化

break、continue、return

1、continue是结束本次循环,继续下一次循环体

2、break是结束循环,循环之后的内容继续执行

3、return则是直接跳出函数,后续的代码都不在执行

尽可能早的break可以优化循环

with

关联了内部变量的对象

with (location) {
    // 相当于location.search等获取
    var qs = search.substring(1); 
    var hostName = hostname;
    var url = href;
}

with会导致性能问题(延长执行环境),且调试困难,不要使用;严格模式下使用with将直接抛出语法错误

函数

1、让函数始终返回一个值,要么永远不要返回值

// bad:有时返回false,有时不返回
function diff(num1, num2) {
    if (num1 < num2) {
        return false;
    } else {
        num1 - num2;
    }
}

// 直接return; 将不会返回值

2、没有重载,后面定义的覆盖前面定义的,即使你的参数不同

3、 通过arguments对象来访问这个参数,注意,arguments对象只是与数组类似(它并不是Array的实例,可以通过arguments.length的形式访问)