JavaScript 学习笔记

引子

最近几个月复习JavaScript时做的笔记,以及一些小问答的记录,后续可能会更新…

JS历史

  • ES3:ECMAScript 3,1999年12月,ECMAScript 3.0版发布,成为JavaScript的通行标准,得到了广泛支持。
  • ES5:ECMAScript 5,2008年发布的标准,由ECMAScript3.1更名而来
  • ES6:2015年6月,ECMAScript 6正式发布,并且更名为“ECMAScript 2015”,有了非常多的新特性和语法糖。

浏览器渲染机制

  • 解析HTML标签,构建DOM树
  • 解析CSS标签,构建CSSOM树
  • 把DOM和CSSOM树合成渲染树
  • 在渲染树的基础上进行布局,计算每个节点的几何结构
  • 把每个节点绘制到屏幕上

Repaint和Reflow

  • 回流(Reflow)
    • 对于DOM结构中的各个元素都有自己的盒子(模型),这些都需要浏览器根据各种样式(浏览器的、开发人员定义的等)来计算并根据计算结果将元素放到它该出现的位置,这个过程称之为reflow
  • 重绘(Repaint)
    • 当各种盒子的位置、大小以及其他属性,例如颜色、字体大小等都确定下来后,浏览器于是便把这些元素都按照各自的特性绘制了一遍,于是页面的内容出现了,这个过程称之为repaint。

加载异步

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

没有 defer 或 async,浏览器会立即加载并执行指定的脚本,“立即”指的是在渲染该 script 标签之下的文档元素之前,也就是说不等待后续载入的文档元素,读到就加载并执行。

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

有 async,加载和渲染后续文档元素的过程将和 script.js 的加载与执行并行进行(异步)。

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

有 defer,加载后续文档元素的过程将和 script.js 的加载并行进行(异步),但 script.js 的执行要在所有元素解析完成之后,DOMContentLoaded 事件触发之前完成。

  • defer:脚本延迟到文档解析和显示后执行,有顺序
  • async:不保证顺序

白屏与FOUC

  • CSS、JS脚本放在页面文档前加载时,浏览器加载CSS、JS等待过程中,就会出现白屏或FOUC
  • Chrome浏览器会处于白屏状态,等待加载内容加载完毕后,展现后面页面的内容,而Firefox浏览器则会展现页面内容,等待CSS加载完毕后闪烁页面,展现带有样式的页面内容

JS数据类型

  • number
  • string
  • boolean
  • undefined
  • null
  • object
  • symbol

数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于undefinednull,一般将它们看成两个特殊值。

判断类型

可以通过typeof运算符判断数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typeof 123 // "number"
typeof '123' // "string"
typeof false // "boolean"
function f() {}
typeof f
// "function"
typeof undefined
// "undefined"
typeof window // "object"
typeof {} // "object"
typeof [] // "object"
// 对象和数组都会返回object,所以可以用instance进行判断
var o = {};
var a = [];

o instanceof Array // false
a instanceof Array // true

运算符

+会有字符串相关处理,-、*、/等会直接把表达式两边都转化为数字。

比较运算符

  • == 不严格相等,不同类型会转换为同类型作比较
  • ===严格相等,不同类型会不等
  • !=不相等
  • !==严格不相等

位运算

  • |按位或
  • &按位与
  • ~取反
  • >>
  • <<
  • >>>

运算符的优先级

运算符的优先级决定了表达式中运算执行的先后顺序,优先级高的运算符最先被执行。

结合性

结合性决定了拥有相同优先级的运算符的执行顺序。考虑下面这个表达式:

1
a OP b OP c

左结合(左到右)相当于把左边的子表达式加上小括号(a OP b) OP c,右关联(右到左)相当于a OP (b OP c)。赋值运算符是右关联的,所以你可以这么写:

1
a = b = 5;

结果 ab 的值都会成为5。这是因为赋值运算符的返回结果就是赋值运算符右边的那个值,具体过程是:b被赋值为5,然后a也被赋值为 b=5 的返回值,也就是5。

汇总表

下面的表将所有运算符按照优先级的不同从高到低排列。

优先级 运算类型 关联性 运算符
20 圆括号 n/a ( … )
19 成员访问 从左到右 … . …
需计算的成员访问 从左到右 … [ … ]
new (带参数列表) n/a new … ( … )
函数调用 从左到右 … ( … )
18 new (无参数列表) 从右到左 new …
17 后置递增(运算符在后) n/a … ++
后置递减(运算符在后) … --
16 逻辑非 从右到左 ! …
按位非 ~ …
一元加法 + …
一元减法 - …
前置递增 ++ …
前置递减 -- …
typeof typeof …
void void …
delete delete …
await await …
15 从右到左 … ** …
14 乘法 从左到右 … * …
除法 … / …
取模 … % …
13 加法 从左到右 … + …
减法 … - …
12 按位左移 从左到右 … << …
按位右移 … >> …
无符号右移 … >>> …
11 小于 从左到右 … < …
小于等于 … <= …
大于 … > …
大于等于 … >= …
in … in …
instanceof … instanceof …
10 等号 从左到右 … == …
非等号 … != …
全等号 … === …
非全等号 … !== …
9 按位与 从左到右 … & …
8 按位异或 从左到右 … ^ …
7 按位或 从左到右 `… …`
6 逻辑与 从左到右 … && …
5 逻辑或 从左到右 `… …`
4 条件运算符 从右到左 … ? … : …
3 赋值 从右到左 … = …
… += …
… -= …
… *= …
… /= …
… %= …
… <<= …
… >>= …
… >>>= …
… &= …
… ^= …
`… = …`
2 yield 从右到左 yield …
yield* yield* …
1 展开运算符 n/a ...
0 逗号 从左到右 … , …

类型转换

类型 结果
Undefined false
Null false
Boolean 直接判断
Number +0, −0, 或者 NaN 为 false, 其他为 true
String 空字符串为 false,其他都为 true
Object true

x == y

x y 结果
null undefined true
Number String x == toNumber(y)
Boolean (any) toNumber(x) == y
Object String or Number toPrimitive(x) == y
otherwise otherwise false

toNumber

type Result
Undefined NaN
Null 0
Boolean ture -> 1, false -> 0
String “abc” -> NaN, “123” -> 123

函数

立即执行函数

在声明时就直接执行的函数,它拥有独立的词法作用域,不仅避免了外界访问此 函数 中的变量,而且又不会污染全局作用域。

1
2
3
4
5
// 立即执行函数
(function () {
console.log('1')
return 1
})()

递归

1
2
3
4
5
6
7
8
// 求n!
function factorial(n) {
if (n === 1) {
return 1
} else {
return n * factorial(n - 1)
}
}

声明提前

1
2
3
4
5
6
7
console.log(a) // 不会报错
var a = 3
// -------->
var a = 3 // 声明前置
console.log(a)

// 同理,函数声明也有类似的特性,但是函数表达式声明方式没有这种特性。

作用域

作用域链

遇到一个变量,在变量的作用域找,找不到则找它的上层作用域。

  1. 函数在执行的过程中,先从自己内部找变量
  2. 如果找不到,再从创建当前函数所在的作用域去找, 以此往上
  3. 注意找的是变量的当前的状态

引用类型

  • 基本类型 数值 布尔值 null undefined,保存在栈内存中
  • 引用类型 对象 数组 函数 正则 存在堆内存中,变量中保存的实际只是一个指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 对象的浅拷贝与深拷贝
var obj = {
name: 'a',
sex: 'male',
age: 22,
friend: {
name: 'b',
age: 33
}
}
// -- 浅拷贝
function copy(oldObj) {
var newObj = {}
for (var i in oldObj) {
if (oldObj.hasOwnProperty(i)) {
newObj[i] = oldObj[i]
}
}
return newObj
}
// -- 深拷贝
function deppCopy(oldObj) {
var newObj = {}
for (var i in oldObj) {
if (typeof oldObj[i] === 'object') {
newObj[i] = deppCopy(oldObj[i])
} else {
newObj[i] = oldObj[i]
}
}
return newObj
}

对象

1
2
3
4
5
6
7
8
var company = {
name: '饥人谷',
age: 3,
sex: '男'
}
for (var key in company) {
console.log(company[key])
}

JSON

  1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
  2. 简单类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null(不能使用NaN, Infinity, -Infinityundefined)。
  3. 字符串必须使用双引号表示,不能使用单引号。
  4. 对象的键名必须放在双引号里面。
  5. 数组或对象最后一个成员的后面,不能加逗号。
1
2
3
4
5
6
7
// 深拷贝
var obj = {
name: 'hunger',
age: 3,
friend: ['aa', 'bb', 'cc']
}
var obj2 = JSON.parse(JSON.stringfy(obj))

数组

1
2
3
4
5
6
7
8
9
10
11
12
13
let arr = [1, 2, 3]

arr.push(4) // [1,2,3,4] 数组末尾添加一个元素
arr.pop() // [1,2,3] 返回值为4 数组末尾删除一个元素
arr.shift() // [2,3] 返回值为1 数组头部删除一个元素
arr.unshift(1) // [1,2,3] 数组头部添加一个元素
let str = arr.join('-') // "1-2-3" 以指定字符为间隔拼接数组为字符串
arr.splice(0, 1, 10) // [10,2,3] 将第0个开始的1个元素替换为10, 可以用来删除数组中的某个位置的元素
arr.sort(function (a, b) {
return a - b
}) // [2,3,10] 原地排序数组
arr.reverse() // [10,3,2] 反转数组序列
let arr2 = arr.concat([1, 2, 3]) // [10,3,2,1,2,3] 返回一个拼接好的新数组,不改变原数组
1
2
3
4
5
6
7
8
9
// 写一个函数,操作数组,返回一个新数组,新数组中只包含正数。
function filterPositive(arr) {
return arr.filter((value) => {
return value > 0 && typeof value === "number"
})
}
var arr = [3, -1, 2, '饥人谷', true]
filterPositive(arr)
console.log(filterPositive(arr)) //[3, 2]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 用 splice函数分别实现 push、pop、shift、unshift方法。
function push(arr, value) {
arr.splice(arr.length, 0, value)
return arr.length
}

function pop(arr) {
return arr.splice(arr.length - 1, 1)[0]
}

function shift(arr) {
if (arr.length > 0) {
return arr.splice(0, 1)[0]
}
}

function unshift(arr, value) {
arr.splice(0, 0, value)
return arr.length
}
var arr = [3, 4, 5]
arr.push(10) // arr 变成[3,4,5,10],返回4
console.log(pop(arr)) // [3,4,5] return 10
console.log(shift(arr)) // [4,5] return 3
console.log(unshift(arr, 2)) // [2,4,5] return 3
console.log(arr) // [2,4,5]

// 改进版
function push(arr, ...args) {
arr.splice(arr.length, 0, ...args)
return arr.length
}

function unshift(arr, ...args) {
arr.splice(0, 0, ...args)
return arr.length
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 对以下代码 users中的对象,分别以 name 字段、age 字段、company 字段进行排序
var users = [{
name: "John",
age: 20,
company: "Baidu"
},
{
name: "Pete",
age: 18,
company: "Alibaba"
},
{
name: "Ann",
age: 19,
company: "Tecent"
}
]

// name
console.log(users.sort(function (a, b) {
return a.name[0] > b.name[0]
}))
// age
console.log(users.sort(function (a, b) {
return a.age - b.age
}))
// company
console.log(users.sort(function (a, b) {
return a.company[0] > b.company[0]
}))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 分别举例说明ES5数组方法 indexOf、forEach、map、every、some、filter、reduce的用法?
let arr = [1, 2, 3, 4]

arr.indexOf(3) // 2 返回指定值在数组中的索引,没有就返回-1,可以用来判断数组是否存在某个元素
arr.indexOf(8) // -1

arr.forEach((element, index, arr) => {
arr[index] = element * element
}) // 便利数组元素,并对数组元素进行指定操作,操作函数接受三个参数:当前元素 当前元素索引 数组
console.log(arr) // [1,4,9,16]

arr2 = arr.map(a => a * a) // 返回一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果,操作函数接受三个参数:当前元素 当前元素索引 数组
console.log(arr2) //[ 1, 16, 81, 256 ]

let isOdd = arr.every(a => a % 2 !== 0) // 测试一个数组内的所有元素是否都能通过某个指定函数的测试。它返回一个布尔值。
console.log(isOdd) // false

let hasOdd = arr.some(a => a % 2 !== 0) // 测试一个数组内是否含有至少一个元素能通过某个指定函数的测试。它返回一个布尔值。
console.log(hasOdd) // true

arr3 = arr.filter(a => a > 10) // filter() 方法创建一个新数组, 其包含通过所提供函数实现的测试的所有元素。
console.log(arr3) // [ 16 ]

sum = arr.reduce((a, b) => (a + b)) // reduce() 方法对数组中的每个元素执行一个提供的reducer函数(升序执行),将其结果汇总为单个返回值。
console.log(sum) // 30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 实现一个reduce函数,作用和原生的reduce类似下面的例子。
// Ex:

// var sum = reduce([1, 2, 3], function(memo, num){ return memo + num; }, 0); => 6

function myReduce(arr, callback, initValue = 0) {
if (Array.isArray(arr) === false) {
return "error"
}
if (initValue !== 0) {
arr.unshift(initValue)
}
if (arr.length === 1) return arr[0]
var temp = callback(arr[0], arr[1])
arr.splice(0, 2, temp)
return myReduce(arr, callback)
}

var sum = myReduce([1, 2, 3], function (memo, num) {
return memo + num;
}, 0);
console.log(sum)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 实现一个flatten函数,将一个嵌套多层的数组 array(数组) (嵌套可以是任何层数)转换为只有一层的数组,数组中元素仅基本类型的元素或数组,不存在循环引用的情况。
// Ex::

// flatten([1, [2], [3, [[4]]]]) => [1, 2, 3, 4];

function flatten(arr) {
var result = []
for (var i = 0; i < arr.length; i++) {
if (Array.isArray(arr[i])) {
result = result.concat(flatten(arr[i]))
} else {
result.push(arr[i])
}
}
return result
}

// reduce 简化
function flatten(arr) {
return arr.reduce(function (prev, next) {
return prev.concat(Array.isArray(next) ? flatten(next) : next)
}, [])
}

console.log(flatten([1, [2],
[3, [
[4]
]]
]))

字符串

1
2
3
4
5
6
7
8
9
10
11
12
// 多行字符串的声明有哪几种常见写法?
var str = 'hello \
world\
!'

var str = `hello
world
!`

var str3 = "hello" +
" world" +
"!"
1
2
3
4
5
6
7
8
9
10
11
var str = 'hello jirengu.com'
// 获取 str 下标为3的字符
console.log(str[3])
//获取 str 下标为4的字符的 Ascii 码
console.log(str.charCodeAt(4))
// 截取字符g到末尾的字符串
console.log(str.slice(str.indexOf('g'), str.length))
// 从字符o开始, 截取长为4个字符的字符串
console.log(str.substr(str.indexOf('o'), 4))
// 获取第一个 l的下标
console.log(str.indexOf('l'))

Math

1
2
3
4
5
6
7
8
9
10
11
12
13
// 写一个函数,生成一个随机 IP 地址,一个合法的 IP 地址为 0.0.0.0~255.255.255.255。

function getRandIP() {
//补全
var arr = [];
for (var i = 0; i < 4; i++) {
var num = Math.floor(Math.random() * 256)
arr.push(num)
}
return arr.join('.')
}
var ip = getRandIP()
console.log(ip) // 10.234.121.45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 写一个函数,生成一个随机颜色字符串,合法的颜色为#000000~ #ffffff。

function getRandColor() {
var color = ''
var dict = '0123456789abcdef'
for (var i = 0; i < 6; i++) {
var index = Math.floor(Math.random() * 16)
var temp = dict[index]
color += temp
}
return color
}
var color = getRandColor()
console.log(color) // #3e2f1b
1
2
3
4
5
6
// 写一个函数,返回从min到max之间的 随机整数,包括min不包括max
function myRandom(min, max) {
if (typeof min !== 'number' || typeof max !== 'number') return 0
return Math.floor(Math.random() * (max - min) + min)
}
console.log(myRandom(7,19))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 写一个函数,生成一个长度为 n 的随机字符串,字符串字符的取值范围包括0到9,a到 z,A到Z。

function getRandStr(len) {
//补全函数
var dict = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
var result = ''
for (var i = 0; i < len; i++) {
var index = Math.floor(Math.random() * 62)
result += dict[index]
}
return result
}
var str = getRandStr(10); // 0a3iJiRZap
console.log(str)

DATE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 写一个函数,参数为时间对象毫秒数的字符串格式,返回值为字符串。假设参数为时间对象毫秒数t,根据t的时间分别返回如下字符串:
// 刚刚( t 距当前时间不到1分钟时间间隔)
// 3分钟前 (t距当前时间大于等于1分钟,小于1小时)
// 8小时前 (t 距离当前时间大于等于1小时,小于24小时)
// 3天前 (t 距离当前时间大于等于24小时,小于30天)
// 2个月前 (t 距离当前时间大于等于30天小于12个月)
// 8年前 (t 距离当前时间大于等于12个月)

function friendlyDate(time) {
var curTime = Date.now()
var duration = curTime - time
var min = 60 * 1000
var hours = min * 60
var day = hours * 24
var month = day * 30
var year = month * 12
switch (true) {
case duration < min:
return '刚刚'
case duration < hours:
return Math.floor(duration / min) + '分钟前'
case duration < day:
return Math.floor(duration / hours) + '小时前'
case duration < month:
return Math.floor(duration / day) + '天前'
case duration < year:
return Math.floor(duration / month) + '个月前'
default:
return Math.floor(duration / year) + '年前'
}
}
var str = friendlyDate('1559203511591')
var str2 = friendlyDate('1558102000000')
console.log(str)
console.log(str2)

正则表达式

贪婪模式和非贪婪模式

贪婪模式

贪婪模式下,正则表达式会尽可能多的匹配重复字符,举例来说:

1
2
3
var str = 'I hate "boring" and "lazy", but i can\'t control myself !';
var reg = /".*"/g;
console.log(str.match(reg)); // [ '"boring" and "lazy"' ]

我们预想的结果是[ '"boring"', '"lazy"' ],而实际结果是[ '"boring" and "lazy"' ],这就是正则的贪婪模式🔒造成的。

  1. 正则先匹配到第一个字符,第一个字符是",很快就匹配到了对应的字符。
  2. 第二个字符是.,他表示任意字符,所以匹配到了b
  3. 第三个是*,他表示匹配前一个表达式0次或者多次,而.表示任意字符,所以会循环匹配到字符串结尾。
  4. 到达字符串结尾了,还没有找到第四个字符",所以,开始回溯,一个一个的缩减所匹配的字符串,直到遇到",匹配结束。
  5. 得到结果[ '"boring" and "lazy"' ]

非贪婪模式

非贪婪模式下,正则表达式会尽可能少的重复匹配字符,举例说明:

1
2
3
4
5
var str = 'I hate "boring" and "lazy", but i can\'t control myself !';
var reg = /".*"/g;
var reg2 = /".*?"/g;
console.log(str.match(reg)); // [ '"boring" and "lazy"' ]
console.log(str.match(reg2));// [ '"boring"', '"lazy"' ]
  1. 第一步与上面的类似,匹配到"
  2. 第二个字符是.,他表示任意字符,所以匹配到了b
  3. 第三步就不同了,在非贪婪模式下,此时会尽可能的匹配",可是b后面的字符是o,所以匹配不上,由.来进行了匹配。
  4. 一直匹配直到遇到了"结束,此时才算匹配到了第一个结果,匹配模式是global的,所以会从此处开始匹配第二个结果。
  5. 最后结果为[ '"boring"', '"lazy"' ]

常见用例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 写一个函数isValidUsername(str),判断用户输入的是不是合法的用户名(长度6-20个字符,只能包括字母、数字、下划线)。

function isValidUsername(str){
let reg = /^[\w]{6,20}$/g // \w 匹配一个单字字符,等价于[a-zA-z_0-9]
return reg.test(str)
}

let str1 = 'my@lllll9970'
let str2 = '12345678901234567890000'
let str3 = 'bert_cai111'
console.log(isValidUsername(str1)) // false
console.log(isValidUsername(str2)) // false
console.log(isValidUsername(str3)) // true

1
2
3
4
5
6
7
8
9
10
11
12
13
// 写一个函数isPhoneNum(str),判断用户输入的是不是手机号。
function isPhoneNum(str) {
let reg = /^1[345789][\d]{9}$/g
return reg.test(str)
}

let phone1 = '17750736159'
let phone2 = '23678878899'
let phone3 = '123456789056789'

console.log(isPhoneNum(phone1)) // true
console.log(isPhoneNum(phone2)) // false
console.log(isPhoneNum(phone3)) // false
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 写一个函数isEmail(str),判断用户输入的是不是邮箱。
// 假设名称只允许单字字符-
function isEmail(str) {
var reg = /^[\w-]+@[\w-]+(\.[\w-]+)+$/g
return reg.test(str)
}

let email1 = 'bert_cai@163.com'
let email2 = '&&&&@163.com'
let email3 = '1234@.999.222.ccc'

console.log(isEmail(email1))
console.log(isEmail(email2))
console.log(isEmail(email3))
1
2
3
4
5
6
7
// 写一个函数trim(str),去除字符串两边的空白字符。
function myTrim(str) {
let reg = /(^[\s]+)|([\s]+$)/g //匹配一个空白字符,包括空格、制表符、换页符和换行符。
return str.replace(reg,'')
}
let str = ' hello world '
console.log(myTrim(str))

\d,\w,\s,[a-zA-Z0-9],\b,.,*,+,?,x{3},^,$分别是什么?

  • \d,数字
  • \w,所有单字字符,包括字母、数字或者下划线
  • \s, 匹配一个空白字符,包括空格、制表符、换页符和换行符
  • [a-zA-Z0-9],匹配一个字母或者数字
  • \b, 匹配一个词的边界,/\bm/匹配“moon”中的‘m’
  • .,匹配除换行符之外的任何单个字符。
  • +,匹配前面一个表达式1次或者多次
  • *,匹配前一个表达式0次或多次
  • ?,匹配前面一个表达式0次或者1次
  • x{3},匹配xxx字符串
  • ^,匹配输入的开始
  • $,匹配输入的结束

定时器

setTimeout:执行一次。

setInterval:执行无数次,必须调用clearInterval停止。

单线程模型

Event loop

callback queue

WebApi

setTimeout会将当前代码移除本次EventLoop,在下次loop时判断条件,满足条件就执行,否则不执行。

JavaScript是单线程的,意思是JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待,JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务,当我们使用setTimeout函数时,setTimeoutcallback就被放到了任务队列。

在执行代码时,主线程会先执行所有的同步任务,等到主线程里的任务都执行完毕后,才会执行任务队列的异步函数,所以setTimeout里的函数,不一定是真的等待了指定时间执行的,如果主线程里的事件耗时太长,等到执行任务队列的回调函数时,已经超出了开始所设置的等待时间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var a = 1;
setTimeout(function () {
a = 2;
console.log(a);
}, 0);
var a; // 重复声明无用,在ES6使用let重复声明还会报错
console.log(a);
a = 3;
console.log(a);

/**
* 输出结果
* 1
* 3
* 2
* 上述代码可以转化为下:
*/

var b // 变量声明
b = 1
console.log(b) // 1
b = 3 // 变量第二次赋值
console.log(b) // 3
b = 2 // 在队列里的代码执行完了,才会判断条件,执行callback里的代码,
console.log(b) // 2
1
2
3
4
5
6
7
var flag = true;
setTimeout(function(){
flag = false;
},0) // 设置定时器,callback需要在eventLoop里的代码执行完毕后才会执行
while(flag){} // flag = true; 陷入死循环
console.log(flag); // 永远不会走到这一步
// 结果为死循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 实现一个节流函数
/**
* 节流
* @param {function} fn 需要节流的函数
* @param {Number} gapTime 需要节流的时间
*/
function throttle(fn, gapTime) {
let lastTime = null
let nowTime = null
return function () {
nowTime = Date.now();
if (!lastTime || lastTime - nowTime > gapTime) {
fn().apply(this, arguments)
lastTime = nowTime
}
}
}

/**
* 防抖
* @param {function} fn 需要防抖的函数
* @param {Number} wait 防抖时间
*/
function debounce(fn, wait) {
let timer = null
return function () {
if (timer) {
clearTimeout(timer)
}
timer = setTimeout(() => {
fn.apply(this, arguments)
}, wait)
}
}

DOM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<ul class="ct">
<li data-img="1.jpg">鼠标放置查看图片1</li>
<li data-img="2.jpg">鼠标放置查看图片2</li>
<li data-img="3.jpg">鼠标放置查看图片3</li>
</ul>
<div class="img-preview"></div>
<script>
//补全代码,要求:当鼠标放置在li元素上,会在img-preview里展示当前li元素的data-img对应的图片。
let imgPanel = document.querySelector('.img-preview')
let liArr = document.querySelectorAll('li')
let temp
console.log(liArr)
liArr.forEach(function (item) {
console.log(item);

item.onclick = function () {
let img = document.createElement('img')
img.src = item.getAttribute('data-img')
if (imgPanel.childNodes.length > 0) {
imgPanel.removeChild(temp)
}
imgPanel.appendChild(img)
temp = img
}
})
</script>
background: #000000;            color: #ffffff;
1
2
3
4
5
6
7
8
9
10
11
12
13
<ul class="ct">
<li class="q2">这里是</li>
<li class="q2">饥人谷</li>
</ul>
<script>
// 有如下代码,要求当点击每一个元素li时控制台展示该元素的文本内容。不考虑兼容。
//todo ...
let liArr2 = document.querySelectorAll('.q2')
console.log(liArr2)
liArr2.forEach(function(item){
console.log(item.innerText)
})
</script>

事件

事件流

  1. IE模型:
事件冒泡模型 事件捕获模型 DOM事件流
div->body->html->document document->html->body->div document->html->body->div->body->html->document

Dom2事件处理程序

  1. addEventListener

  2. removeEventListener

  3. 接受三个参数

    1. 事件类型
    2. 事件处理函数
    3. 布尔值,true表示在事件捕获阶段触发,false表示在事件冒泡阶段触发,默认为false
  4. 优势:能够绑定多次处理程序,后面的不会覆盖前面的

  5. `javascript
    btn.addEventListener(‘click’,function(){

    console.log('helllo')
    

    })

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22

    ### 事件冒泡

    ### 常见的HTML事件

    ```javascript
    click
    dblclick // 双击左键
    mouseover // 鼠标移入 移入子元素也会触发一次out与over
    mouseout // 鼠标移出
    mousenter // 鼠标移入 移入子元素不会出发enter和leave
    mousleave // 鼠标移出
    focus // 获取焦点
    blur // 失去焦点
    keyup // 按键抬起
    change // 值的变动
    submit // form 提交事件
    scroll // 滚动 触发多次
    resize // 窗口发生变化 触发多次
    onload // 页面所有资源加载完成触发
    DOMContentLoaded // DOM结构渲染完成

事件传播机制

DOM2级事件定义了两个方法用于处理指定和删除事件处理程序的操作:

  1. addEventListener
  2. removeEventListener

所有的DOM节点都包含这两个方法,并且它们都接受三个参数:

  1. 事件类型
  2. 事件处理方法
  3. 布尔参数,如果是true表示在捕获阶段调用事件处理程序,如果是false,则是在事件冒泡阶段处理

事件传播主要分为两个部分,一个是捕获阶段,一个是冒泡阶段,假设我们有如下四个部分:

document、html、body、div

在事件的捕获阶段,事件是从外层往内层传播的,顺序如下:

document—>html—>body—>div

在事件冒泡阶段,事件是从内层逐级像外层冒泡的,顺序如下:

div—>body—>html—>document

还有一个阶段,处于目标阶段,它处在捕获与冒泡间,此时目标实际接收事件。

阻止传播

在事件的传播过程中中途中断事件传播,让其在指定元素层停止。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

// 具体元素以上面的为例
function $(selector) {
return querySelector(selector)
}
// 阻止捕获
$('html').addEventListener('click',function(e){
console.log('html catch')
},true)
$('body').addEventListener('click',function(e){
console.log('body catch')
e.stopPropagation() // 在body就阻止了
},true)
$('div').addEventListener('click',function(e){
console.log('div catch')
},true)

// 阻止冒泡
$('html').addEventListener('click',function(e){
console.log('html bubble')
},false)
$('body').addEventListener('click',function(e){
console.log('body bubble')
},false)
$('div').addEventListener('click',function(e){
console.log('div bubble')
e.stopPropagation() // 在div这就阻止了
},false)

取消默认事件

某些元素,例如a元素,当用户点击时会自动跳转到ahref所指的链接,我们可以通过设置,阻止这个跳转操作,进行地址检测,只允许部分链接跳转。

1
2
3
4
5
6
7
$('a').onclick = function (e) {
e.preventDefault()
console.log(this.href)
if (/qq.com/.test(this.href)) {
location.href = this.href
}
}

事件代理

把一个元素的响应事件函数绑定到另一个元素上,就叫做事件代理。

举个例子,我们也买呢有n个divs元素,我们需要给这些div元素添加点击事件,点击时打印出标签的innerText,同时我们还有一个按钮会在点击时添加新的div,这时候,新添加的div元素的点击事件就不那么好设置了,我们可以通过把div的点击事件函数绑定到div的父元素container上,这就是一个典型的事件代理的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$('#container').onclick = function(e){
console.log(this)
console.log(e.target)
if(e.target.classList.contains('box')){
console.log(e.target.innerText)
}
}

$('#add').onclick = function(e){
var box = document.createElement('div')
box.classList.add('box')
box.innerText = 'hello'
$('#container').appendChild(box)
}

onlickaddEventListener的区别

  1. 使用方式不同,onlick属于dom对象的一个属性,使用时直接通过赋值方式将一个响应函数传递给onlick,以此来作为事件响应。addEventListener时对象的一个方法,接受三个参数:
    1. 事件类型
    2. 事件处理函数
    3. 布尔值,true表示在事件捕获阶段触发,false表示在事件冒泡阶段触发,默认为false
  2. 作用次数不一样,onlick方式只能绑定一个响应函数,后面绑定的响应函数会覆盖前面的,addEventListener方式可以绑定多次处理程序,后面的不会覆盖前面的。
1

BOM

window.onloaddocument.onDOMContentLoaded 有什么区别?

  • window.onload设置的函数会在load触发时执行,此时,在文档中的所有对象都在DOM中,所有图片,脚本,链接以及子框都完成了装载。
  • document.onDOMContentLoaded:当初始的 HTML 文档被完全加载和解析完成之后,DOMContentLoaded 事件被触发,而无需等待样式表、图像和子框架的完成加载
1
2
3
4
5
6
7
8
9
// 如何获取图片真实的宽高?
var imgNode = document.querySelector('img')
imgNode.onload = function(){
console.log(imgNode.offsetHeight) // height
console.log(imgNode.offsetWidth) // width
}
//如何获取元素的真实宽高?
window.getComputedStyle(imgNode).height
window.getComputedStyle(imgNode).width

HTMLElement.offsetHeight 是一个只读属性,它返回该元素的像素高度,高度包含该元素的垂直内边距和边框,且是一个整数。

1
2
3
4
5
6
7
8
// URL编码解码
// encode
encodeURI() // 不会编码ascii字母数字和常见特殊字符~!@#$&*()=:/,;?+'
encodeURIComponent() // 与上面相比,会编码@#$&:,?+=/ 编码范围更大
// decode 上述两个encode的逆
decodeURI()
decodeURIComponent()

为什么要编码

首先,因为网络标准RFC 1738做了硬性规定:

“…Only alphanumerics [0-9a-zA-Z], the special characters “$-.+!*’(),” [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL.”
“只有字母和数字[0-9a-zA-Z]、一些特殊符号”$-
.+!*’(),”[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。”

然后在某些时候,url里面的参数可能是另一个url,此时需要使用encodeURIComponent(),将”$””&””=”等等字符也进行编码,避免出现错误。

Cookie 和 locakstorage存储的值都是字符串

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie使基于无状态的HTTP协议记录稳定的状态信息成为了可能。

Cookie主要用于以下三个方面:

  • 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
  • 个性化设置(如用户自定义设置、主题等)
  • 浏览器行为跟踪(如跟踪分析用户行为等)

Cookie曾一度用于客户端数据的存储,因当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie渐渐被淘汰。由于服务器指定Cookie后,浏览器的每次请求都会携带Cookie数据,会带来额外的性能开销(尤其是在移动环境下)。

session

session使用中能够让服务器识别某个用户的机制,一般存储在服务器内存或者数据库中,实现session一般需要使用cookie,一个常见的使用机制如下

  • 创建session后,将session_id通过setCookie添加到http响应头中。
  • 浏览器在加载页面时发现响应头又set-cookie字段,就把该cookie加到浏览器指定域名下。
  • 下次刷行域名时,请求会带上cookie,服务器更具cookie中的session_id来识别用户。

localStorage

  1. localStorage HTML5本地存储web storage特性的API之一,用于将大量数据(最大5M)保存在浏览器中,保存后数据永远存在不会失效过期,除非用 js手动清除。
  2. 不参与网络传输。
  3. 一般用于性能优化,可以保存图片、js、css、html 模板、大量数据。

AJAX

Asynchronous JavaScript + XML(异步的JavaScript+XML),本身并不是一种新技术,依赖于现有的CSS/HTML/JavaScript,其中最核心的依赖是浏览器提供的XMLHttpRequest对象,这个对象使得浏览器可以发送HTTP请求或者接受HTTP响应,实现在页面不刷新的情况下与服务器进行数据交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 简单封装一个ajax
function ajax(opts) {
let url = opts.url
let type = opts.type || 'GET'
let dataType = opts.dataType || 'JSON'
let onsuccess = opts.onsuccess || function () { }
let onerror = opts.onerror || function () { }
let data = opts.data || {}

let dataStr = []
for (let key in data) {
dataStr.push(key + '=' + data[key])
}

dataStr = dataStr.join('&')
if (type === 'GET') {
url = url + '?' + dataStr
}

let xhr = new XMLHttpRequest()
xhr.onload = function (e) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
if (dataType === 'JSON') {
onsuccess(JSON.parse(xhr.responseText))
} else {
onsuccess(xhr.responseText)
}
} else {
console.log("ERROR: " + xhr.status)
}
}
xhr.open(type, url)
if (type === 'POST') {
xhr.send(dataStr)
} else {
xhr.send()
}
xhr.onerror = onerror
}

MOCK

mock数据有三种方式:

  1. 文件访问,将需要获取的数据直接存在文件里,访问时直接访问对应的静态文件
  2. 搭建本地mock服务器,模拟数据
  3. 使用第三方mock网站,例如easymock,rap等

跨域

同源策略

浏览器处于安全策略考虑,只允许与本域下的接口交互,不同源的客户端脚本在没有明确授权的情况下,不能读写对方的资源。

同源

  • 同协议
  • 同域名
  • 同端口

对于当前页面来说页面存放的JS文件的域不重要,重要的是加载该JS页面所在什么域

注意,同源策略是浏览器的安全策略。

实现跨域

JSONP

HTML中的script标签可以加载其他域的js。

JSONP通过script标签加载数据的方式,获取数据当作JS执行

CORS

IE10+,前端W加入一个Origin头,后台返回数据后加入一个Access-Control-Allow-Origin头

降域

闭包

测试题

如下代码输出多少?如果想输出3,那如何改造代码?

1
2
3
4
5
6
7
var fnArr = [];
for (var i = 0; i < 10; i ++) {
fnArr[i] = function(){
return i
};
}
console.log( fnArr[3]() )
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
for (var i = 0; i < 10; i++) {
fnArr[i] = (function (j) {
return function () {
return j
}
})(i)
}

for (var i = 0; i < 10; i++) {
(function (j) {
fnArr[j] = function () {
return j
}
})(i)
}

for (let i = 0; i < 10; i++) {
fnArr[i] = function () {
return i
}
}

封装一个 Car 对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var Car = (function(){
var speed = 0;
//补充
return {
setSpeed: setSpeed,
getSpeed: getSpeed,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.setSpeed(30)
Car.getSpeed() //30
Car.speedUp()
Car.getSpeed() //31
Car.speedDown()
Car.getSpeed() //30
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var Car = (function () {
var speed = 0;
//补充
function setSpeed(speed) {
this.speed = speed
}

function getSpeed() {
console.log(this.speed)
return this.speed
}

function speedUp() {
this.speed++
}

function speedDown() {
this.speed--
}
return {
setSpeed: setSpeed,
getSpeed: getSpeed,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.setSpeed(30)
Car.getSpeed() //30
Car.speedUp()
Car.getSpeed() //31
Car.speedDown()
Car.getSpeed() //30

如下代码输出多少?如何连续输出 0,1,2,3,4?

1
2
3
4
5
for(var i=0; i<5; i++){
setTimeout(function(){
console.log('delayer:' + i )
}, 0)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log('delayer: ' + i)
}, 0)
}
// 输出5个5
for (var i = 0; i < 5; i++) {
setTimeout((function (j) {
return function () {
console.log('delayer: ' + j)
}
})(i), 0)
}

for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log('delayer: ' + j)
},0)
})(i)
}

for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log('delayer: ' + i)
})
}

补全代码,实现数组按姓名、年纪、任意字段排序。

1
2
3
4
5
6
7
8
var users = [
{ name: "John", age: 20, company: "Baidu" },
{ name: "Pete", age: 18, company: "Alibaba" },
{ name: "Ann", age: 19, company: "Tecent" }
]

users.sort(byField('age'))
users.sort(byField('company'))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var users = [{
name: "John",
age: 20,
company: "Baidu"
},
{
name: "Pete",
age: 18,
company: "Alibaba"
},
{
name: "Ann",
age: 19,
company: "Tecent"
}
]

function byField(field) {
return function (user1, user2) {
return user1[field] > user2[field]
}
}

users.sort(byField('age'))
console.log(users)
users.sort(byField('company'))
console.log(users)

写一个 sum 函数,实现如下调用方式。

1
2
console.log( sum(1)(2) ) // 3
console.log( sum(5)(-1) ) // 4
1
2
3
4
5
6
7
8
function sum(i) {
return function (j) {
return i + j
}
}

console.log(sum(1)(2)) // 3
console.log(sum(5)(-1)) // 4

JQuery

图片懒加载

  1. 设置图片默认动画,一般设置成一个loading图。

  2. 通过判断滚动容器视窗高度height,容器滚动高度scrollTop,图片相对于容器的高度offsetHeight三个值,heigth+scrollTop>=offsetHeight时表示图片出现在了用户视野中,此时加载图片。

  3. 滚动事件可以使用函数节流的方式减少事件触发次数,提高效率,已加载图片可以设置标

    志,减少图拍链接修改次数,提高效率。

瀑布流布局

  1. 通过容器宽度和需要进行布局的元素宽度计算除瀑布列数
  2. 设置一个数组,数组长度就是瀑布列数,值时对应列数的高度
  3. 当一个新的待布局元素加载时,使用绝对定位,将其拼接到高度最低的列后面
  4. 依次加载,就实现了一个瀑布流布局

木桶布局的实现原理

  1. 设置容器宽度,和容器基准高度。
  2. 将第一个待布局的图片进行对基准高度进行等比缩放,放在第一行。
  3. 依次将后面的图片放到第一行,当最后一个图片放入时会导致超过容器宽度时将其放到第二行,同时将第一行的所有图片作为一个整体,等比拉伸(可以对row使用flex布局),使其宽度等于容器宽度。
  4. 依次渲染,实现木桶布局。

JS面向对象

如下代码中, new 一个函数本质上做了什么?

1
2
3
4
function Modal(msg){
this.msg = msg
}
var modal = new Modal()
  1. 新建一个空对象,空对象的__proto__就是Modal.prototype
  2. 将this的初始化的值传递到这个对象中
  3. 将这个对象返回给modal

画出如下代码的原型图。

1
2
3
4
5
6
7
8
9
10
function People (name){
this.name = name;
}

People.prototype.walk = function(){
console.log(this.name + ' is walking');
}

var p1 = new People('饥人谷');
var p2 = new People('前端');

扩展 String 的功能增加 reverse 方法,实现字符串倒序

1
2
3
var str = 'hello jirengu'
var str2 = str.reverse()
console.log(str2) // 'ugnerij olleh'
1
2
3
String.prototype.reverse = function () {
return this.split('').reverse().join('')
}

apply、call 、bind有什么作用,什么区别

  • apply传入两个参数,一个作为函数的目标对象,一个数组作为函数的参数
  • call传入一个对象作为函数的目标对象,然后传入多个参数作为函数参数
  • bind传入以一个对象,返回一个新的函数,新函数的this指向传入的对象

call()方法的作用和 apply() 方法类似,区别就是call()方法接受的是参数列表,而apply()方法接受的是一个参数数组。

有如下代码,解释Person、 prototype、proto、p、constructor之间的关联。

1
2
3
4
5
6
7
8
function Person(name){
this.name = name;
}
Person.prototype.sayName = function(){
console.log('My name is :' + this.name);
}
var p = new Person("若愚")
p.sayName();

  • Person: 函数Person
  • constructor: Person.prototype的constructor指向Person
  • __proto__: p.__proto__ === Person.prototype
  • p是Person的一个实例

对String做扩展,实现如下方式获取字符串中频率最高的字符

1
2
3
var str = 'ahbbccdeddddfg';
var ch = str.getMostOften();
console.log(ch); //d , 因为d 出现了5次
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
String.prototype.getMostOften = function () {
let obj = {}
let most = 0
let mostChar = ''
for (let i of this) {
if (obj[i]) {
obj[i]++
if (obj[i] > most) {
most = obj[i]
mostChar = i
}
} else {
obj[i] = 1
}
}
console.log(obj, most, mostChar)
return mostChar
}
作者

bert_cai

发布于

2019-07-31

更新于

2019-07-31

许可协议

评论