背景
1995 年 Netscape 公司的 Brendan Eich 创造了 Javascript。
1996 年 Netscape 公司将 JavaScript 提交给了标准化组织 ECMA。ECMA 发布了 262 号文件即 ECMA-262,将这种语言标准命名为 EMCAScript,由 ECMA 下属的 TC39 技术委员会编写。TC39 的成员包含了各主流浏览器厂商。
基本构成
JS = ES + DOM + BOM
运行环境
也可以叫使用场景
客户端
浏览器 | 渲染引擎(即浏览器内核) | JS 引擎 |
---|---|---|
Chrome | Blink | V8 |
Edge | ||
Opera | ||
QQ 浏览器等国内浏览器 | ||
Android 移动端浏览器 | ||
Safari | Webkit | JavaScriptCore |
iOS 移动端浏览器 | ||
Firefox | Gecko | |
IE | Trident |
Google 早期使用 Webkit 引擎,后从 Webkit 创建 Blink 分支。
不同 JS 引擎对 ES 标准实现程度不一样,V8 原是 C++开源项目,后被 Google 收购并开发。
Q:Chromium 属于哪一类,和 Chrome 是什么关系?
A:Chromium 不属于三类中任何一类,只是 Google 的一个开源的浏览器项目,Chrome 就是在 Chromium 的基础上封装了更多的功能,国内大多数浏览器都是基于 Chromium 开发的。
服务端
基于 V8 引擎的 NodeJS 环境,可进行服务端编程并提供后端服务能力。
ECMAScript
JS 的语法标准
各浏览器厂商基于 ECMAScript 标准,实现 JS 引擎即解释器对 JS 代码解释执行。
Q:JavaScript 与 ECMAScript 的关系?
A:ES 是标准,JS 是实现。
TC39 技术委员会每年 6 月发布一个 ES 版本,版本号以年份命名,例如 ES2015(即 ES6)
版本发布流程:提案(Proposal)->审批->标准(Standard),其中又细分为以下 5 个阶段。
阶段 | 含义 | 备注 |
---|---|---|
stage0 | Strawman-展示阶段 | |
stage1 | Proposal-征求意见阶段 | |
stage2 | Draft-草案阶段 | 进入 stage2 的提案最后基本都会成为标准 |
stage3 | Candidate-候选阶段 | |
stage4 | Finish-结案阶段 |
大小写
区分大小写,包括关键字,例如 typeof 是关键字,但 typeOf 是可用变量名(虽然不建议用)。
分隔符
符号 | 含义 | 备注 |
---|---|---|
; | 分号 | 句末可以省略分号,但有些场景需要 |
' | 单引号 | |
" | 双引号 |
Q: 哪些场景下需要分号
A:场景一: [].slice.apply()前面需要分号,因[]开头与前面语句难以断句;
场景二:IIFE 立即执行函数前面需要分号,(function(){}())
和 (function(){})()
前面(上句末或本句前)需要有分号
模式
模式名称 | 指定方式 | 备注 |
---|---|---|
普通模式 | 默认 | |
严格模式 | 首行增加预处理指令'use strict'; | ES5 引入的概念,语法要求更严格,编译时即报错 例如 var 变量必须先定义再使用否则报错 |
变量
定义
类型 | 作用域 | 声明方式 | 备注 |
---|---|---|---|
var | 函数作用域 | var xxx = 'hello' | 可重复声明 |
let | 块级作用域 | let xxx = 'hello' | 不能重复声明 |
const | 块级作用域 | const xxx = 'hello | 不能重复声明,声明时需要初始化,声明后不能修改 |
var
已不建议使用 var,作用域污染
如果在全局作用域下(即不在函数块中)设置 var,和通过 window.xxx=yyy 设置相同,可通过 window.xxx 获取,而 let/const 在全局下定义虽然不能通过 window 获取到,但实际仍是全局作用域。
作用域提升
不论在函数什么位置用 var 声明变量,编译时都会提升到函数最开头。即在 var 声明之前打印变量不会报错,会打印 undefined。而且可以重复声明。
let/const 不存在作用域提升,因此上述情况会报错。
let
块级作用域
块级作用域由最近的一对{}
界定范围,函数作用域更小例如 for 的{}就形成了一个块级作用域,每次循环都是一个块级作用域。
ES6 之前最小直到函数作用域,循环绑定事件需要通过 IIFE 才能形成自己的块级作用域,不会被块外面修改变量而影响;ES6 之后循环如果使用块级作用域的变量,每一次循环都会创建独立的变量。
const
const 优先于 let 使用,便于编译时静测
声明后不能修改,对于基础类型变量不能修改值,而对于引用类型是不能修改引用地址,属性值可以增删改。
数据类型
基础类型
6 类,存储在栈中
类型 | 含义 |
---|---|
undefined | 未定义 |
null | 空值 |
String | 字符串 |
Number | 数字 |
Boolean | 布尔值 |
Symbol | 符号 |
undefined
变量未声明或已声明未初始化时的类型。可以用void 0
来表示 undefined,避免无意间修改 undefined。
null
Q:null
和 undefined
区别
A:undefined 其实是 window 下的全局变量,而 null 是关键字
null == undefined; // true
null === undefined; // false
typeof null; // 'object'
typeof undefined; // 'undefined'
String
// 定义
let str = "world";
属性
str.length
const str = "hello";
// 返回字符串长度,即字符个数
str.length; // 5
方法
str.toLowerCase()
const str = "Hello";
// 返回全部字符转换为小写的字符串
const newStr = str.toLowerCase(); // 'hello'
str.charAt()
const str = "hello";
// 返回指定索引位置的字符
const newStr = str.charAt(0); // 'h', 同样适用于中文字符串,会获取一个汉字
str.trim()
const str = " hello ";
// 返回去除收尾两端空白符的新字符串
const newStr = str.trim(); // 'hello'
str.indexOf(searchValue [, fromIndex])
const str = "hello";
// 返回搜索的子串所在索引位置, 未查找到返回-1
str.indexOf("el"); // 1
str.includes(searchString[, position])
const str = "hello";
// 返回是否在字符串中查找到字串
str.includes("el"); // true
str.replace(regexp|substr, newSubStr|function)str.replace
const str = "hello";
let newStr = "";
// 返回替换的新字符串
newStr = str.replace("e", "l"); // 'hlllo'
newStr = str.replace(/l/gi, "c"); // 'hccco'
str.replaceAll(regexp|substr, newSubstr|function)
替换所有,如果是正则表达式,则必须带修饰符 g
str.match(regex)
const str = "hello.doc";
// 匹配到返回数组,未匹配到返回null
const result1 = str.match(/\.docx?$/); // [.doc]
const result2 = str.match(/\.docx$/); // null
str.split([separator[, limit]])
const str = "hello world";
// 返回拆分后组成的数组
const arr = str.split(" "); // ['hello', 'world']
str.substring(indexStart[, indexEnd])
const str = "hello";
// 返回截取的字符串,indexEnd缺省默认截取到结尾
const newStr1 = str.substring(0); // 'hello'
// 返回的字符串不包含indexEnd位置的字符
const newStr2 = str.sbustring(1, 2); // 'e'
str.substr(start[, length])
已被废弃,应该改用 substring
const str = "hello";
// 返回截取的字符串
const newStr = str.substr(1, 2); // 'el'
str.startsWith(searchString[, position])
const str = "hello";
// 返回是否以'he'开头
console.log(str.startsWith("he")); // true
类型转换
// 其他类型转换成String
// 方式一,适用于非null和undefined的类型,因为null和undefined没有toString方法
xxx.toString();
// 方式二,适用于所有类型
String(xxx);
字符串拼接
let str1 = "world";
// 字符串拼接
// 方式一
let str2 = "hello" + str1;
// 方式二
let str3 = `hello${str1}`;
Number
let num = 0;
// 其他类型转换成Number,一般只针对本身字符串数字,否则转换后为NaN
// 方式一
Numer(xxx);
// 方式二
parseInt(xxx);
parserFloat(xxx) +
// 方式三,针对字符串
xxx;
取值范围
Number.MAX_VALUE; // 正值最大值1.7976931348623157e+308,计算结果大于最大值返回Infinity
Number.MIN_VALUE; // 正值最小值5e-324,即0.0000..5,计算结果小于最小值返回-Infinity
浮点数
// 因为精度问题,不能直接比较两个浮点数
0.1 + 0.2 == 0.3; // false,因为0.1+0.2将得到0.30000000000000004
// 浮点数比较方法,原理:相减小于最小精度则相等
Math.abs(0.1 + 0.2 - 0.3) <= Number.EPSILON;
NaN
用于运算失败的返回结果,不是数字
1 / 0; // Infinity
0 / 0; // NaN
// 判断NaN只能通过isNaN,不能通过===
isNaN(xxx);
科学计数法
例如:3.12e5 或 3.12e+5,即 3.12*10 的 5 次方,312000
3.12e-5,即 3.12*10 的-5 次方,0.0000312
Boolean
// 其他类型转换成Boolean,针对字符串'true'或'false'转换成布尔值
// 方式一
Boolean("true"); // true
// 方式二
JSON.parse("true"); // true
Symbol
ES6 新增,可以理解为不会重复的值
Symbol()生成唯一记号,可用于确保对象的 key 唯一,实际使用场景不多。
// 局部定义
// 定义,Symbol()是函数而不是构造函数,不能使用new,区别于String/Boolean/Number
let sym1 = Symbol();
// 可以增加描述信息,xxx是描述信息,描述信息相当于注释说明,对值生成没有任何影响,每次返回值不相同
let sym2 = Symbol("xxx");
// 使用
let obj = {
[sym1]: "cc",
[sym2]: 18,
};
console.log(obj[sym1]);
// 全局定义
// 创建,全局唯一,具有幂等性,执行多次只会创建一次,每次返回相等
let sym = Symbol.for("xxx");
引用类型
存储在堆中,所有引用类型都继承自是 Object,typeof 引用类型都返回'object'
基础引用类型
类型 | 含义 |
---|---|
Date | 日期 |
RegExp | 正则表达式 |
Math | 数学运算 |
Global | |
包装类型 | String Boolean Number |
Date
提供关于日期和时间的信息,包括当前日期、时间及相关计算。
const date = new Date();
// 获取当前日期时间
const now = `${date.getFullYear()}-${
date.getMonth() + 1
}-${date.getDate()} ${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; // 2020-10-29 16:34:25
// 获取当前时间戳毫秒值
const timestampNow = Date.now(); // 1652917948262
实例方法
date.getMonth()
const date = new Date();
// 返回当前月份的索引,取值范围[0-11],例如1月的索引是0
const monthIndex = date.getMonth(); // 4
date.getMinutes()
const date = new Date();
// 返回当前分钟的索引,取值范围[0-59]
const minuteIndex = date.getMinutes(); // 5,当前时间是x点过6分
RegExp
提供正则表达式能力。
定义
// pattern是正则表达式,attributes是正则模式符(例如i和g)
// 方式一
const regex1 = /pattern/attributes
// 方式二
csont regex2 = new RegExp(pattern, attributes)
元字符
元字符 | 含义 | 备注 |
---|---|---|
. | 匹配任意一个字符 | 英文句点 |
[] | 匹配中括号内任意一个字符 | |
\d | 数字 | |
\D | 非数字 | |
\w | 数字、字母、下划线 | |
\s | 空白字符 | 小写 s |
\S | 非空白字符 | 大写 S |
\b | 单词边界 | |
* | 匹配 0 次到 n 次 | |
+ | 匹配 1 次到 n 次 | |
? | 匹配 0 次或 1 次 | |
非贪婪模式修饰符 | ||
^ | 匹配开头 | |
中括号中取反 | 例如:匹配非数字[^\d] | |
& | 匹配结尾 | |
{} | {n} 匹配 n 次 | |
{n,} 匹配至少 n 次 | ||
{n,m} 匹配 n 到 m 次 |
Q:(a|b|c)和[abc]的区别
A:匹配结果一致,注意|
只能用在小括号中
实例方法
regex.test(str)
// 返回是否字符串与正则匹配
const str = 'hello.doc'
/.+\.docx?$/.test(str) // true
/.+\.xlsx?$/.test(str) // false
贪婪模式/非贪婪模式
const str = "<h1>正则表达式</h1>";
// 贪婪模式
const strNew1 = str.replace(/<.*>/g, ""); // ''
// 非贪婪模式
const strNew2 = str.replace(/<.*?>/g, ""); // '正则表达式'
断言
只是正则的额外条件,不实际匹配字符
先行断言
浏览器普遍支持
const str = 'hello'
// ?=xxx 匹配紧跟xxx的
// 匹配紧跟o的l,会匹配的第二个l
/l(?=o)/.test(str) // true
// ?!xxx 匹配不紧跟xxx的
// 匹配不紧跟o的l,会匹配的第一个l
/l(?!o)/.test(str) // true
后行断言
ES9 才开始支持,浏览器普遍不支持,且没法通过 Babel 编译,因此 Babel 也会报错
const str = 'hello'
// ?<=xxx 匹配前面有xxx的
// 匹配前面有e的l,会匹配第一个l
/(?<=e)l/.test(str) // true
// ?<!xxx 匹配前面没有xxx的
// 匹配前面没有e的l,会匹配第二个l
/(?<!e)l/.test(str) // true
常用正则规则
匹配类型 | 正则 | 备注 |
---|---|---|
匹配手机号 | ^1\d{10}$ | |
匹配汉字 | ^[\u4e00-\u9fa5]*$ | 空串或全部是汉字 |
[\u4e00-\u9fa5] | 有汉字 | |
匹配空行 | ^\s*\n | linux&mac 的换行符是\n \s 是*而不是+,因为空白行也可能只有一个换行符没有\s |
^\s*\r\n | windows 的换行符是\r\n |
Math
提供数学运算能力。
注意:Math 对象上提供的计算要比直接在 JavaScript 实现的快得多,因为 Math 对象上的计算使用了 JavaScript 引擎中更高效的实现和处理器指令。但使用 Math 计算的问题是精度会因浏览器、操作系统、指令集和硬件而异。
静态方法
Math.round(x)
// 四舍五入
Math.round(1.5); // 2
Math.ceil(x)
// 向上取整
Math.ceil(1.5); // 2
Math.floor(x)
// 向下取整
Math.floor(1.5); // 1
Math.random()
// 返回随机数(0到1之间的小数,含0不含1)
Math.random(); // 例如0.6108911334124585
Global
下图所列都是 Global 的属性,Global 无法直接访问,浏览器将 window 对象实现为 Global 对象的代理,即访问 window 相当于访问了 Global,当然 window 的作用远不止于此。
包装类型
// String/Number/Boolean对应的包装类型,声明方式不一样,例如:
// 基础类型的包装类型声明
let numObj = new Number(0);
// 基础类型声明
let num = 0;
numObj === num; // false
typeof num; // 'number'
typeof numObj; // 'object'
num.constructor === Number; // true
numObj.constructor === Number; // true
集合引用类型
Object
对象一组属性的无序集合,相当于一张散列表,属性由键名和键值组成,键名只能是字符串或 Symbo 类型,键值可以是任何数据类型或函数。
let obj = { name: "cc", age: 18 }; // name是key,cc是value
console.log(obj.name); // 'cc'
Q:对象的 key 重复会报错吗?
A:不会,如果 key 重名,只会保留相同 key 的最后一个键值对
隐藏属性
属性 | 含义 | 备注 |
---|---|---|
configurable | 是否可使用 delete 将该属性删除 | 一旦修改为 false,则无法再改成 true 且其他配置项无法再配置 |
enumerable | 是否可以通过 for-in 循环 | |
writable | 值是否可修改 | |
value | key 对应的 value |
const obj = {};
// 等价于Object.defineProperty(obj, 'name', {value: 'cc', writable: true, ...}),隐藏属性默认全是true
obj.name = "cc";
// 注意这样缺省配置隐藏属性默认全是false
Object.defineProperty(obj, "age", { value: 18 });
// 通过Object.defineProperty()进行修改,例如
Object.defineProperty(obj, "name", { value: "Sim", writable: false });
// 查看对象属性的隐藏属性
Object.getOwnPropertyDescriptors(obj); // {name: {value: 'cc', writable: false, ...}}
静态方法
Object.assign(target, ...sources)
const obj = { name: "cc", sex: "01" };
// 返回target,Object.assign(target, source1, source2, ...)
const newObj1 = Object.assign(obj, { name: "Sim", age: 18 }); // newObj1是obj的浅拷贝,obj本身也已改变, {name: 'Sim', sex:'01'}
const newObj2 = Object.assign({}, obj); // newObj2是obj的深拷贝,{name:'cc', sex: '01'}
Object.defineProperty(obj, prop, descriptor)
const obj = { name: "cc", sex: "01" };
// 设置对象的隐藏属性及setter和getter
Object.defineProperty(obj, "name", {
value: "Sim",
writable: false,
set(value) {
name = value;
// 相当于添加一个钩子,在设置对象时触发,Vue响应式即基于此
console.log(value);
},
get() {
return name;
},
});
Object.keys(obj)
const obj = { name: "cc", sex: "01" };
// 返回对象所有key组成的数组
const keys = Object.keys(obj); // ['name', 'sex']
解构
ES6 新增语法
const obj = { name: "cc", age: 18, sex: "01" };
const { name, age } = obj;
// 等价于:
// const name = obj.name
// const age = obj.name
console.log(name); // 'cc'
// 反之亦可
const name = "Sim";
const age = 20;
const obj2 = { name, age };
// 深层解构
const deepObj = { name: "cc", level: { badminton: 2, swimming: 1 } };
const {
level: { badminton, swimming },
} = deepObj;
// 等价于:
// const badminton = deepObj.level.badminton
// const swimming = deepObj.level.swimming
console.log(badminton); // 2
Array
定义
// 声明
let arr1 = []; // 建议
let arr2 = new Array();
let arr3 = new Array(10);
// 声明同时初始化
let arr4 = [1, 2]; // 建议
let arr5 = new Array(1, 2);
// 预留特定索引位置
let arr6 = [1, , 3]; // 索引0的值是1,索引1的值是undefined,索引2的值是3
// 数组中元素可以是不同类型
let arr7 = [1, "hello", { name: "cc" }];
属性
arr.length
const arr = [1, 2, 3];
// 返回数组长度
console.log(arr.length); // 3
静态方法
Array.isArray(obj)
// 返回布尔值xxx是否为数组, ES6新增
Array.isArray(xxx); // 任意类型xxx都不会报错
Array.from(_arrayLike_[, _mapFn_[, _thisArg_]])
// 将类似数组的数据结构转换成数组,例如可以转换NodeList, ES6新增
Array.from(xxx);
实例方法
arr.toString()
const arr = [1, 2, 3];
// 返回逗号分隔的字符串
const str = arr.toString(); // '1,2,3'
arr.slice([begin[, end]])
const arr = [1, 2, 3]
// 返回包含索引0到1(不含)的数组,注意是浅拷贝
const newArr1 = arr.slice(0, 1) // [1]
// 默认返回从索引0到结尾的数组
const newArr2 = arr.slice() // [1,2,3]
// 返回索引倒数第2个到索引倒数第1个(不含)的数组
const newArr3 = arr.slice(-2, -1) // [2]
// 索引前后关系错误返回空数组
const newArr4 = arr.slice(-1, -2) // []
// 特殊用法:类数组转换为数组,ES6可以直接使用Array.from(likeArr)
[].slice.apply(likeArr) // 将likeArr的各项添加到[]中, 同[].slice.call(likeArr)
arr.splice(start[, deleteCount[, item1[, item2[, ***...]]]]*)**
let arr = [];
// 数组中删除或新增项
// 从索引0开始删除1项
let arr = [1, 2, 3];
arr.splice(0, 1); // 原数组变成[2,3] 并返回[1]
// 在索引0处新增元素,删除0项
arr = [1, 2, 3];
arr.splice(0, 0, "cc"); // 原数组变成['cc',1,2,3] 返回[]
// 替换索引0处的元素
let arr = [1, 2, 3];
arr.splice(0, 1, 10); // 原数组变成[10,2,3] 返回[1]
// 在索引0处新增另外一个数组中的项,注意数组要展开
arr = [1, 2, 3];
let arr2 = [4, 5, 6];
arr.splice(0, 0, ...arr2); // 原数组变成[4,5,6,1,2,3]
// 默认参数从指定索引位置开始删除直到结尾
arr = [1, 2, 3];
arr.splice(1); // 原数组变成[1], 返回[2,3]
arr.indexOf(searchElement[, fromIndex])
const arr = [1, 2, 3];
// 在数组中查找元素(基础类型), 返回索引
const num = arr.indexOf(1); // 0
arr.findIndex(callback[, thisArg])
const arr = [{ name: "cc" }, { name: "Sim" }];
// 在数组中查找一个对象, 返回对象所在索引
const obj = { name: "cc" };
const num = arr.findIndex((item) => item.name == obj.name); // 0
arr.includes(valueToFind[, fromIndex])
const arr = [1, 2, 3];
// 返回是否找到某元素
const bool = arr.includes(1); // true
arr.shift()
let arr = [1, 2, 3];
// 弹出数组中第一个元素
arr.shift(); // 原数组变成 [2, 3],并返回1
arr.unshift(element1, ..., elementN)
let arr = [1, 2, 3];
// 在数组开头插入元素, 返回数组新长度
arr.unshift(-1, 0); // 原数组变成[-1, 0, 1, 2, 3], 并返回5
arr.push(element1, ..., elementN)
let arr = [1, 2, 3];
// 在数组尾部插入元素, 返回数组新长度
arr.push(4, 5); // 返回5, 原数组变成[1, 2, 3, 4, 5]
arr.pop()
let arr = [1, 2, 3];
// 弹出数组中最后一个元素
arr.pop(); // 原数组变成[1, 2],并返回3
arr.reverse()
let arr = [1, 2, 3];
// 逆序重排,原数组改变
arr.reverse(); // 原数组变成[3, 2, 1]
arr.reduce((previousValue, currentValue, currentIndex, array) => { /* ... */ }, initialValue)
const arr = [1, 2, 3];
// 对整个数组做累加或其他操作,initial指定时total使用initialValue,否则total使用第1个元素,空数组时不指定会报错
const num = arr.reduce((total, currentValue) => total + currentValue, 0); // 6
特殊用法
console.log(+[]); // 0
Set
ES6 新增
与数组相比,Set 不包含重复值(引用类型则是引用地址不能相同),且没有索引(但有序),因此不能像数组一样直接通过索引获取某个值,而只能通过迭代器按顺序获取。
// 创建
let s1 = new Set();
// 从数组创建
let arr = [1, 2, 3];
let s2 = new Set(arr);
// 从类数组NodeList创建
let divs = document.querySelectorAll("div");
let s3 = new Set(divs);
实例属性
set.size
let set = new Set();
// 返回Set的大小,即元素的个数
console.log(set.size); // 0
实例方法
set.add(value)
let set = new Set();
// 添加元素到末尾
set.add("cc");
set.has(value)
let set = new Set();
set.add("cc");
// 返回是否包含某元素
set.has("cc"); // true
set.delete(value)
let set = new Set();
set.add("cc");
// 删除元素,返回true表示有此元素被删除,false表示没有找到此元素
set.delete("cc"); // true
set.clear()
let set = new Set();
set.add("cc");
// 清空所有元素
set.clear();
set.values()
set.keys()
set.entries()
// 返回可迭代对象(用于for..of遍历),为了与Map类型保持一致,由于Set没有key只有value,把key也赋值成了value
set.values();
set.keys(); // 是values方法的别名
set.entries(); // key和value相同
特殊用法
// 查看html中总共有多少种标签
let tagCount = new Set(
Array.from(document.querySelectorAll("*")).map((elem) => elem.nodeName)
);
WeakSet
只能是对象的集合,且集合中对象的引用为弱引用( 如果没有其他的对WeakSet
中对象的引用,那么这些对象会被当成垃圾回收掉)
Map
key 可以是任意类型
let map = new Map();
实例属性
map.size
let map = new Map();
map.set("name", "cc");
// 返回map中键值对的数量
map.size; // 1
实例方法
map.set(key, value)
map.get(key)
let map = new Map();
// 设置键值对
map.set("name", "cc"); // key是字符串情况
let obj = { name: "Sim" };
map.set(obj, true); // key还可以是对象等任意类型
// 返回key对应value
map.get("name"); // 'cc'
map.get(obj); // true
注意 Map 类型不支持 JSON 序列化
let map = new Map();
map.set("name", "cc");
JSON.stringify(map); // 返回空{}
Q:Map 和 Object 的区别
A:如下表
Object | Map | ||
---|---|---|---|
key 类型 | String 或 Symbol | 任意类型 | |
元素个数 | 手动计算 | map.size | |
赋值 | obj.xxx = yyy | map.set(xxx, yyy) | Map 虽然能像 Object 一样赋值,但无法用自身实例方法获取到,因此必须用 set 方法赋值 |
WeakMap
相比 Map 类型,WeakMap 的 key 只能是对象类型,更重要的是 WeakMap 不会影响垃圾回收,即 WeakMap 中的对象如果在外部的所有使用到的地方都没有了,即可被垃圾回收掉,不会受 WeakMap 中使用了该对象影响,因此能够避免内存泄露。
运算符
! | 逻辑反 | 返回布尔值,存在类型转换 例如 null/undefined/0/NaN/''/false 都返回为 true |
---|---|---|
!! | 逻辑双重取反 | 返回布尔值,存在类型转换 例如 null/undefined/0/NaN/''/false 都返回为 false |
void | 将任何类型的表达式转换成 undefined | |
&& | 逻辑与 | |
|| | 逻辑或 | |
... | 数组/对象等展开 | 返回数组/对象中所有元素 const arr1 = [1, 2, 3] const arr2 = [...arr1] |
条件
if
if(条件1){
...
}else if(条件2){
...
}else{
...
}
switch
switch(变量){
case 字符串1:
...
// 没有break会继续执行后面的代码
break
// 可针对不同的字符串使用相同的处理逻辑,前提是该case没有内容体
case 字符串2:
case 字符串3:
...
break
default:
...
break
}
&&
// xx不为空时,a=xx
// xx为空时,a=yy
let a = xx || yy;
||
// xx不为空时,a=yy
// xx为空时,a=xx
let a = xx && yy;
&&
和||
可以结合使用,简化条件语句
循环
for
xxx 可以是数组、对象、字符串
for (let i = 0; i < xxx.length; i++) {
console.log(xxx[i]);
}
for-in
xxx 可以是数组、对象,xxx 是数组时 key 同时也是索引值
for (const key in xxx) {
console.log(xxx[key]);
}
for-of
xxx 可以是数组、Set 等可迭代对象
for (const item of xxx) {
console.log(item);
}
// 获取索引值的写法
for (const [i, item] of xxx.entries()) {
console.log(item);
}
退出循环
关键字 | 含义 | 备注 |
---|---|---|
continue | 退出本次循环,继续循环往后 | |
break | 退出本层循环 | 注意由于 switch 中也有 break,如果 for 中嵌套 switch,switch 中的 break 不会触发退出本层循环 |
return | 退出多层循环 |
以下为数组的函数式编程遍历方式:
forEach
arr.forEach((item, i) => {
console.log(item);
});
every
遍历数组检查是否所有元素都满足指定条件,满足返回 true,否则返回 false
const result = arr.every((item) => item.id === id);
filter
遍历数组,返回符合条件数组元素组成新数组
const newArr = arr.filter((item) => item.id === id);
findIndex
遍历数组,返回第一个符合条件的数组元素索引,常用于对象数组中查找符合条件的对象
const index = arr.findIndex((item) => item.id === id);
map
遍历数组,常用于返回对象数组中对象的指定属性组成新数组
const newArr = arr.map((item) => item.id);
Q:循环遍历时操作数组对循环是否有影响
A:对数组的索引和值的对应关系有影响,因为删除改变了数组元素顺序,需要注意此问题可能导致循环不充分或不符合预期,注意规避
函数
函数都是 Function 类型的实例。
(function () {}.constructor === Function); // true
typeof function () {}; // 'function'
声明
5 种声明方式
普通函数
function hello() {
console.log("Hello World");
}
匿名函数
字面量
将匿名函数赋值给变量
const hello = function () {
console.log("Hello World");
};
箭头函数
普通匿名函数的简化写法
const hello = () => {
console.log("Hello World");
};
参数
值传递
// str1和obj1是实参(实际参数),str和obj是形参(形式参数)
//
function hello(str, obj) {
// 基础类型传递的是值
str += " world"; // 参数str变成了hello world', str1还是'hello'
// 引用类型传递的是引用地址, 即形参和实参栈地址不同,但指向同一堆地址
obj.name = "Sim"; // obj和obj1的name都变成了Sim
obj = {}; // 形参obj变成了{},而实参obj1还是{name:'Sim'}
}
let str1 = "hello";
let obj1 = { name: "cc" };
hello(str1, obj1);
默认值
可以直接在参数上赋默认值。
function hello(str1, str2 = "world") {
console.log(str1 + str2);
}
// 可缺省传值,使用默认值
hello("hello");
实例方法
func.apply(thisArg, [argsArray])
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
// 调用sum函数,所有参数放到一个数组中
return sum.apply(this, [num1, num2]);
}
console.log(callSum(1, 2)); // 3
func.call(thisArg, arg1, arg2, ...)
function sum(num1, num2) {
return num1 + num2;
}
function callSum(num1, num2) {
// 调用sum函数,所有参数单个传递
return sum.call(this, num1, num2);
}
console.log(callSum(1, 2)); // 3
Q:apply 和 call 与直接调用函数有什么区别?
A:使用 call()或 apply()的好处是可以将任意对象设置为任意函数的作用域,这样对象可以不用关心方法。
window.color = "red";
let o = { color: "blue" };
function sayColor() {
console.log(this.color);
}
sayColor(); // red
sayColor.call(this); // red
sayColor.call(window); // red
sayColor.call(o); // blue
func.bind(thisArg[, arg1[, arg2[, ...]]])
解决 this 指向问题
function func1(a, b, c) {
return a + b + c;
}
// 基于原函数生成新函数,原函数的拷贝
// 此例只是演示,没有改变this指向
const func2 = func1.bind(this);
console.log(func2(10, 20, 30)); // 60
// 但注意与func2=func1相比,此方法相当于深拷贝
console.log(func2 == func1); // false
// 可配置参数
const func3 = func1.bind(this, 10);
console.log(func3(20, 30)); // 60
立即执行
IIFE,Immediately Invoke Function Expression
作用
声明和执行一条语句执行,作用域完全隔离。
写法
// 方式一
(function () {
console.log("hello");
})()(
// 方式二
(function () {
console.log("hello");
})()
);
闭包
一个能访问外部函数作用域内变量的函数
作用
let 出现之前,常用的一种保护私有变量的机制,在函数执行时形成私有的作用域,保护里面的私有变量不受外界干扰。相当于 Java 中通过 getter 和 setter 操作私有变量。
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
var btns = document.getElementsByTagName("button");
for (var i = 0; i < btns.length; i++) {
// 结合函数立即执行传递参数
(function (i) {
btns[i].addEventListener(
"click",
// 闭包形成私有作用域,让i成为私有变量不受外界干扰
function () {
console.log(">>>This is ", i);
}
);
})(i);
}
场景
- 防抖(或节流)中的应用
通过闭包实现不需要全局变量也可以记录状态
const debounce = function (fn, mills) {
let context = null;
// 返回函数赋值给onclick, 即此时onclick的回调函数已经是返回的函数而不是debounce
return function () {
if (!context) {
fn();
} else {
clearTimeout(context);
}
context = setTimeout(() => {
context = null;
}, mills);
};
}
btn.onclick = debounce(() => {
// ...
}, 3000);
异步执行
回调函数
// 1000毫秒后执行function,默认0毫秒,但实际含义是0毫秒后放入任务队列待执行
const hello = setTimeout(function () {
console.log("hello");
}, 1000);
Promise
ES6 新增,可以简化异步接口的有序执行,解决嵌套回调层次太深引发的「回调地狱」问题
Promise 对象用于表示一个异步操作的最终完成 (或失败)及其结果值,是对尚不存在结果的一个替身。
一个 Promise
必然处于以下几种状态之一:
待定(pending): 初始状态,既没有被兑现,也没有被拒绝。
已兑现(fulfilled): 意味着操作成功完成。
已拒绝(rejected): 意味着操作失败。
创建
const helloPromise = new Promise((resolve, reject) => {
// resolve前的代码是同步代码
// ...
// resolve(someValue) // fulfilled
// 或
// reject("failure reason") // rejected
});
Q:Promise 和 Axios 的关系
A:Axios是 HTTP 请求工具,Promise 是异步执行机制,HTTP 请求都是异步请求,所以会用到 Promise
async await
简化 Promise 异步写法,用同步的写法写异步代码
async function hello(){
// await语句后面的语句,会等待await执行完才执行,相当于then,但简化了写法
// 1. await只能出现在async函数中
// 2. 只能接promise对象(async函数或者new Promise)
await hello2()
await hello3()
}
async hello2(){
console.log('hello2')
}
hello3(){
return new Promise(..)
}
面向对象
类
ES6 新增的class
关键字只是一个语法糖,用来定义类,实际还是基于原型。例如:
class Point {
constructor(x, y) {
this.x = x
this.y = y
}
toString() {
return `(${this.x}, ${this.y})`
}
}
// 等价于
Point.prototype = {
constructor() { //... },
toString() { //... }
}
构造函数
关键字是constructor
,在 class 定义中非必需,默认为空。解释器在处理 new 操作符创建类的新实例时,会调用 constructor 关键字修饰的函数。
constructor
中的this
指向新对象,成员变量可以不需要单独声明,直接在 constructor 中通过 this.xx 定义和初始化。当然也可以跟 Java 一样直接在 class 中先定义。
属性
类中声明属性不需要 let/const/function 修饰
属性分为状态
和行为
-
状态
实例变量
静态变量
getter/setter
-
行为
实例方法
静态方法
实例变量
一般无需直接定义,在constructor
中直接通过this.xxx
定义即可。
静态变量
在 class 中使用 static 声明静态变量当前尚未正式纳入 ES 标准中
在 class 外部定义,例如Point.info='hello'
实例方法
class Point {
// ...
hello() {
console.log("hello");
}
// ...
}
const point = Point();
point.hello(); // 'hello'
静态方法
class Point {
// ...
static helloStatic() {
console.log("hello static");
}
// ...
}
Point.helloStatic(); // 'hello static'
对象
每个实例化的对象会继承原型的属性和方法
对象的载体即 Object 类型。
迭代器
不需要再以索引为基础来构建循环集合,例如for-of
的应用
其他
with
让作用域变的复杂,不建议使用,可用解构代替
with (location) {
// search、hostname、href都是window.location下的变量,with可以简化写法
console.log(search);
console.log(hostname);
console.log(href);
}
DOM
文档对象模型
HTML 编程接口,即 JS 与 HTML 交互的接口。
BOM
浏览器对象模型。
浏览器编程接口,即 JS 与浏览器交互的接口。例如 JS 中通过 window.location 获取浏览器页面访问的地址并进行操作。BOM 的顶级对象是 window 对象,JS 通过 window 对象对 DOM 和浏览器进行编程。
Q:BOM 和 DOM 的关系?
A:window.document 即 DOM 对象,可以说 BOM 包含了 DOM,但 DOM 标准是 W3C 制定的,BOM 的 document 是浏览器的实现。相当于浏览器在实现 DOM 标准的同时增加了对浏览器自身的编程接口,即 BOM=DOM+浏览器编程接口。
window:浏览器对象
NodeJS 的 global 对象即相当于 BOM 的 window。
全局对象
所有全局对象都属于 window,以下两种方式定义全局变量
// 方式一:不通过var/let/const声明的变量都是window下的全局变量
hello1 = "js";
console.log(window.hello1); // 'js'
// 方式二:全局下使用var声明的变量是window下的全局变量。
var hello2 = "js";
console.log(window.hello2); // 'js'
窗口操作
window.parent
父窗口对象,适用于在 iframe 中使用,如果没有父窗口,则等同于 window。
window.opener
当前窗口的被打开窗口对象,适用于在被 window.open()打开的窗口中使用。
window.scrollTo(x-coord, y-coord)
// 窗口滚动到指定坐标,例如顶部
window.scrollTo(0, 0);
window.scrollTo(options)
options
top
上边距
left
左边距
behavior
'auto': 默认,直接跳转到指定位置
'smooth': 平滑滚动到指定位置
// 窗口滚动到指定位置,例如平滑滚动到顶部
window.scrollTo({ top: 0, left: 0, behavior: "smooth" });
window.open(url, target, windowFeatures)
url
target
'_balnk': 默认,浏览器新标签打开
注意:
windowFeatures
非空才会在浏览器新窗口打开
windowFeatures
width
窗口宽度
height
窗口高度
注意:宽高需要同时设置才生效
// 新标签打开百度
window.open("https://baidu.com");
// 新窗口(500px宽高)打开百度
window.open("https://baidu.com", "_blank", "width=500, height=500");
场景
网站上微信登录
微信扫码登录只能扫描微信官方页面上的二维码,没办法直接拿到二维码并展示
需求
登录页
点击微信登录按钮,弹出新窗口打开微信扫码登录页面(微信官方页面)
扫码登录后跳转
首页
自动关闭弹窗,并刷新页面 A 显示已登录状态
实现
登录页:基于 window.open 打开微信登录窗口
const url = "xxx"; // 微信扫码登录地址
window.open(url, "_blank", "width=500, height=500, top=200, left=500");
首页:弹窗中微信登录成功后跳转首页(url 参数中增加from=weixin
),在首页里写关闭弹窗和重新加载登录页的逻辑
注意:需要根据是否存在 window.opener 即是否来自弹窗来触发事件,否则会影响首页正常访问逻辑
import Qs from "qs";
const query = Qs.parse(window.location.search.substring(1));
// 是否来自微信登录后跳转
const isFromWeixin = query.from === "weixin";
// 来自微信登录跳转且来自弹窗
if (isFromWeixin && window.opener) {
// 关闭弹窗
window.close();
// 刷新打开弹窗的页面(即登录页),基于cookie或localstorage获取已登录状态
window.opener.location.reload();
}
window.close()
// 关闭当前窗口(浏览器标签页)
window.close();
URL 操作
window.location
属性
window.location.href
// 页面跳转到指定URL
window.location.href = "https://baidu.com";
// 获取当前URL
console.log(window.location.href); // 'https://baidu.com'
包含关系
- href
- orgin
- protocol
- host
- hostname
- port
- pathname
- search
- hash
- orgin
方法
window.location.replace(url)
替换当前页面。返回上一页不会返回跳转前的页面,因为 history 中已经把上一个页面在队列中替换了。
window.location.replace("https://baidu.com");
window.location.reload()
当前页面重载。相当于浏览器 F5。
window.location.reload();
window.location.assign(url)
跳转,效果同给 href 赋值。history 中会把跳转的作为最后一项,跳转前后面的 history 将都被清除掉。
window.location.assign("https://baidu.com");
window.history
针对浏览器单个标签页而言,用户访问页面的队列,访问新页面才+1,且返回不会-1
方法
window.history.go()
页面前进后退,不会刷新页面。
// 当前页重载,两种方式
window.history.go();
window.history.go(0);
// 后退一页
window.history.go(-1);
// 前进一页
window.history.go(1);
window.history.back()
后退一页,同 window.history.go(-1),也同
window.history.back();
window.history.forward()
前进一页,同 window.history.go(1)
window.history.forward();
window.history.pushState()
window.history.replaceState()
其他操作
window.navigator.userAgent
console.log(window.navigator.userAgent); // 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
window.btoa(stringToEncode)
window.atob(encodedData)
// base64编码
const encodedData = window.btoa("hello");
// base64解码
const decodedData = window.atob(encodedData);
window.encodeURI(str)
转换为%开头的十六进制用于作为 URI 进行传输,主要用于转换汉字用于 URI 上传输
不会对
字母
数字
~!@#$&*()=:/,;?+'
编码
实际上 POST 采用 x-www-form-urlencoded 就已经进行了编码,GET 的 URL 传参当前主流浏览器也会自动编码,不需手动处理
const str1 = ";,/?:@&=+$"; // 保留字符
const str2 = "-_.!~*'()"; // 不转义字符
const str3 = "#"; // 数字标志
const str4 = "ABC abc 123"; // 字母数字字符和空格
console.log(encodeURI(str1)); // ;,/?:@&=+$
console.log(encodeURI(str2)); // -_.!~*'()
console.log(encodeURI(str3)); // #
console.log(encodeURI(str4)); // ABC%20abc%20123
console.log(encodeURIComponent(str1)); // %3B%2C%2F%3F%3A%40%26%3D%2B%24
console.log(encodeURIComponent(str2)); // -_.!~*'()
console.log(encodeURIComponent(str3)); // %23
console.log(encodeURIComponent(str4)); // ABC%20abc%20123
window.encodeURIComponent(str)
不会对
字母
数字
~!*()'
编码,即相对于 encodeURI,encodeURIComponent 会转义/
#
?
这三个 URL 中会出现的字符
window.eval(string)
// 执行字符串JS语句
eval("console.log('hello')"); // 'hello'
window.postMessage(message, targetOrigin, [transfer])
窗口间通信。一个窗口可以获得对另一个窗口的引用(比如 targetWindow = window.opener
),然后在窗口上调用 targetWindow.postMessage()
方法分发一个 MessageEvent
消息。
// 场景一:iframe和父窗口(window.parent)
// 1. 在iframe中发起向parent通信
const targetWindow = window.parent;
// message是要传递的参数对象
const message = { name: "cc", age: 18 };
// targetOrigin是要消息接收方所在域,如果不设置跨域情况会通信失败
const targetOrigin = "*"; // 注意:设置成*是存在安全隐患的,任何origin都能监听到此message
// 向targetOrigin发送消息
targetWindow.postMessage(message, targetOrigin);
// 2. parent中监听,注意只能在window下监听,不能指定的元素
// 为避免此事件被注册多次导致重复操作,此处建议使用DOM0事件模型写法绑定事件
window.onmessage = function (e) {
// 开发调试时避免vue-devtools影响
if (e.data.source && e.data.source.startsWith("vue-devtools")) {
return;
}
console.log(e.data); // 传递过来的数据,即{name: 'cc', age: 18}
console.log(e.origin); // 发送方窗口(此场景下即iframe)的 origin(域名、协议和端口),例如http://xxx.com,可用于校验身份
console.log(e.source); // 发送方窗口对象的引用(此场景下即iframe),可以使用此来在两个窗口之间建立双向通信
};
// 场景二:新窗口和父窗口(window.opener),例如微信登录窗口与网站的通信
提示: 有些场景下 iframe 页面中的内容可能也是一个独立访问的页面,不需要传递消息否则会报错,就要增加触发条件,例如通过 URL 传参来标识页面访问的来源
if(this.queryString.appFrom == 'school-workflow'){targetWindow.postMessage(...)}
注意:用于接收消息的任何事件监听器必须首先使用 origin 和 source 属性来检查消息的发送者的身份,因为不检查 origin 和 source 属性会可能导致跨站点脚本攻击。
原型
class 的 prototype 和 class 实例的__proto__的关系