webpack

webpack-demo

1
2
3
4
yarn init
yarn add webpack webpack-cli --dev

npx webpack --version

在 package.json 中添加

1
2
3
4
"scripts": {
"build": "rm -rf dist && webpack",
"start": "webpack serve --open"
},

webpack-dev-server

1
yarn add webpack-dev-server --dev

在 webpage.config.js 中添加

1
2
3
4
devtool: 'inline-source-map',
devServer: {
static: './dist',
},

html-webpack-plugin

1
yarn add html-webpack-plugin --devex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const HtmlWebpackPlugin = require('html-webpack-plugin');
const path = require('path');

module.exports = {
entry: 'index.js',
output: {
path: path.resolve(__dirname, './dist'),
filename: 'index.[contenthash].js'
},
plugins: [new HtmlWebpackPlugin({
title: 'My App',
template: 'src/assets/index.html'
})],
};

css-loader

1
yarn add css-loader --dev
1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};

style-loader

1
yarn add style-loader --dev
1
2
3
4
5
6
7
8
9
10
module.exports = {
module: {
rules: [
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
};

mini-css-extract-plugin

1
yarn add mini-css-extract-plugin --dev
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module.exports = {
plugins: [new MiniCssExtractPlugin({
filename: '[name].[contenthash].css'
})],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
}
]
}
};

sass-loader

1
yarn add sass dart-sass --dev
1
2
3
4
5
6
7
8
9
test: /\.scss$/i,
use: [
"style-loader",
"css-loader",
{
loader: "sass-loader",
options: { implementation: require("dart-sass") }
}
]

less-loader

1
yarn add less less-loader --dev
1
2
test: /\.less$/i,
use: ["style-loader", "css-loader", "less-loader"]

stylus-loader

1
yarn add stylus stylus-loader --dev
1
2
test: /\.styl$/i,
use: ["style-loader", "css-loader", "stylus-loader"]

file-loader

1
yarn add file-loader --dev
1
2
test: /\.(png|svg|jpg|gif)$/,
use: ["file-loader"]

path.resolve和__dirname

异步

  • 如果能直接拿到将结果

    那就是同步

    比如你在医院挂号,你拿到号才会离开窗口

    同步任务可能消耗 10 毫秒,也可能需要 3 秒

    总之不拿到结果你是不会离开的,这就是同步

  • 如果不能直接拿到结果

    那就异步

    比如你在餐厅门口等位,你拿到号后可以去干别的事比如逛街

    你可以每 10 分钟取餐厅问一下排到自己了没(轮询)

    也可以扫码用微信接受通知(回调)

异步举例

  • 以 AJAX 为例

    request.send() 之后,并不能直接得到 response

    console.log(request.response) 试试

    必须等到 readyState 变为 4 后,浏览器才会回头调用 request.onreadystatechange 函数

    我们才能得到 request.response

    这就跟餐厅给你发微信提醒的过程类似

  • 回调 callback

    你写给自己用的函数,不是回调

    你写给别人用的函数,就是回调

    request.onreadystatechange 就是写个浏览器调用的

    意思是你(浏览器)回头调用一下这个函数

  • 简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getJSON.onclick = () => {
const request = new XMLHttpRequest();
request.open('GET', '/5.json');
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
const object = JSON.parse(request.response);
myName.textContent = object.name;
console.log(request.response);
}
}
request.send();
// console.log(request.resopne)
setTimeout(() => {
console.log(request.response);
// 或者将 放在 request.onreadystatechange 函数中
}, 2000)
}
// request.send()执行完,再执行 request.onreadystatechange 函数
// 执行顺序为 response.open() => respone.send() => request.onreadystatechange

onreadystatechange 执行两次

回调

写了却不调用,给别人调用的函数,就是回调。需自行意会

函数例子:

1
2
3
4
5
function f1() {}
function f2(fn) {
fn()
}
f2(f1)

分析:

  1. 我调用 f1 没有?
  2. 我把 f1 传给 f2 (别人)了没有?
  3. f2 调用 f1 了没有?

答:1. 调用了。2.穿了。3.f2 调用了 f1。

那么,f1 是不是我写给 f2 调用的函数? 是。

所以,f1 是回调。


例子2:

1
2
3
4
5
6
7
8
function f1(x) {
console.log(x)
}
function f2(fn){
fn('hello')
}
f2(f1)
// hello

fn(‘hello’) 中的 fn 就是 f1 。

fn(’hello‘) 中的 ’hello’ 会被赋值给参数 x 。

所以 x 就是 ‘hello’。

异步和回调的关系

  • 关联

    异步任务需要再得到结果时通知 JS 来拿结果

    怎么通知?

    可以让 JS 留一个函数地址给浏览器(电话号码)

    异步任务完成时浏览器调用该函数即可(拨打电话)

    同时把任务作为参数传给该函数(通知)

    这个函数是我写给浏览器调用的,所以是回调函数

  • 区别

    异步任务需要用到回调函数来通知结果

    但回调函数不一定只用在异步任务里

    回调可以用到同步任务里

    array.forEach(n=>console.log(n)) 就是同步回调

判断同步异步

如果一个函数的返回值处于

  • setTimeout
  • AJAX(即 XMLHttpRequest)
  • AddEventListener

这三个东西内部中,那么这个函数就是异步函数

还有其他 API 是异步的,遇到再说。

举例说明:

摇骰子, 随机打印出1-6 中的一个数

1
2
3
4
5
6
function 摇骰子() {
setTimeout( () => { // 箭头函数
return parseInt(Math.random() * 6) + 1;
}, 1000)
// return undefined
}

分析:

摇骰子() 里没有写 return,那就是 return undefined

箭头函数里有 return,返回真正结果

所以这是一个异步函数/异步任务。

1
2
const n = 摇骰子();
console.log(n)

如何拿到异步结果?

用回调,写个函数,然后把函数地址给他

1
2
3
4
5
6
7
8
9
10
11
function f1(x) {
console.log(x);
}
// 然后要求 摇骰子函数 得到结果后 把结果作为参数 传给f1
function 摇骰子(fn){
setTimeout( () => {
fn(parseInt(Math.random() * 6) + 1)
}, 1000)
}

摇骰子(f1); // 结果

简化为箭头函数

1
2
3
4
5
6
7
8
9
10
11
function f1(x) { 
console.log(x);
}
摇骰子(f1);
// 改为
摇骰子(x => {
console.log(x);
});
// 再简化为
摇骰子(console.log);
// 如果参数个数不一致就不能这样简化,有个相关面试题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 面试题
const array = ['1', '2', '3'].map(parseInt);
console.log(array);
// 结果为 [1, NaN, NaN]
// parseInt('1', 0, arr) => 1
// parseInt('2', 1, arr) => NaN
// parseInt('3', 2, arr) => NaN

const array = ['1', '2' , '3'].map((item, i , arr) => {
return parseInt(item)
});
console.log(array)
// 正确结果 [1, 2, 3]

// 简写 永远使用箭头函数
const array = ['1', '2', '3'].map((item) => parseInt(item));

异步总结

  • 异步任务不能拿到结果
  • 于是我们传一个回调给异步任务
  • 异步任务完成时调用回调
  • 调用的时候把结果作为参数

异步任务两个结果,成功或失败

两个方法解决

方法一:回调接受两个参数

1
2
3
4
5
6
fs.readFile('./1.txt', (error, data) => {
if(error){
console.log('失败'); return;
}
console.log(data.toString()) // 成功
})

方法二:两个回调

1
2
3
4
5
6
ajax('get', '/1.json', data => {/*成功回调*/}, error => {/*失败回调*/})
ajax('get', '/1.json',{
success: () => {},
fail: () => {}
})
// 接受一个对象,对象有两个 key 表示成功和失败

这些方法的不足

不管方法一还是方法二,都有问题

  1. 不规范,名称五花八门,有人用 success + error,有人用 success + fail,done + fail
  2. 容易出现==回调地狱== ,代码变得看不懂
  3. 很难进行错无处理

回调地狱举例

1
2
3
4
5
6
7
8
9
getUser( user => {
getGroups(user, (groups) => {
groups.forEach( (g) => {
g.filter(x => x.ownerId === user.id)
.forEach(x => console.log(x))
})
})
})
// 仅示例,这只是四层,二十层呢,代码会很难读
Snipaste_2021-09-25_22-12-10
吐槽回调地狱 来源网络

如何解决回调问题,用 Promise

有什么办法能解决这三个问题:

  • 会犯回调的名字或顺序
  • 拒绝回调地狱,让代码可读性更强
  • 很方便地捕获错误

1976年,Daniel P.Friedman 和 David Wis 俩人提出 Promise 思想

后人基于此发明了 Future、Delay、Deferred等

前端结合 Promise 和 JS,制定了 Promise/A+规范

该规范详细描述了 Promise 的原理和使用方法。

以 AJAX 的封装为例,来解释 Promise

Promise MDN

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
// 示例
ajax = (method, url, options) => {
const {success, fail} = option; // 析构赋值
// const succes = option.success;
// const fail = option.fail;
const request = new XMLHttpRequest();
request.open(method, url);
request.onreadystatechange = () => {
if (request.readyState === 4) {
// 成功就调用 success,失败 fail
if (request.status < 400) {
success.call(null, request.response);
}else if (request.status >= 400) {
fail.call(null, request, request.status);
}
}
}
request.rend();
}

ajax('get', '/xxx', {
success(response){}, // function 缩写
// success: function(response){}
fail: (request, status) => {}
})

Promise 写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 先改一下调用姿势
ajax('get', '/xxx', {
success(response){},
fail: (request, status) => {}
})
// 上面用到了两个回调,还使用了 success 和 fail

// 改成 Promise 写法
ajax('get', '/xxx')
.then((response)=>{}, (request, status)=>{} )
// 虽然也是回调
// 但是不需要经济 success 和 fail 了
// then 的第一个参数就是 success
// then 的第二个参数就是 fail

请问 ajax() 返回了个啥 ?

返回了一个含有 .then() 方法的对象呗。

那么再请问如何得到这个含有 .then() 的对象呢 ?

那就要改造 ajax 的源码了。

return new Promise((resolve, reject) => {})

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 修改 ajax
ajax = (method, url, option) => {
return new Promise((resolve, reject) => {
const {success, fail} = option;
const request = new XMLHttpRequest();
request.open(methon, url);
request.onreadystatechange = () => {
if(request.readyState === 4) {
if(request.status < 400) {
// 成功
resolve.call(null, request.response);
}else if(request.status >= 400) {
// s
reject.call(null, request);
}
}
}
request.send();
});
}

小结

第一步:

  • retrun new Promise((resolve, reject) => {...})
  • 任务成功则调用 resolve(result)
  • 任务失败则调用 reject(error)
  • resolve 和 reject 会再去调用成功和失败函数

第二步:

  • 使用 .then(success, fail)传入成功函数和失败函数

我们自己封装的 ajax 的缺点:

post 无法上传数据。request.send(这里可以上传数据)

不能设置请求头。request.setRequestHeader(key, value)

使用:

Jquery.ajax

axios(推荐)

选择排序

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

const minIndex = (numbers) => {
var index = 0
for (let i = 1; i < numbers.length; i++) {
if (numbers[i] < numbers[index]) {
index = i
}
}
return index
}

const swap = (array, i, j) => {
let temp = array[i]
array[i] = array[j]
array[j] = temp
}

const sort = (numbers) => {
for (let i = 0; i < numbers.length - 1; i++) {
console.log(`----`)
console.log(`i: ${i}`)
let index = minIndex(numbers.slice(i)) + i
console.log(`index: ${index}`)
console.log(`min: ${numbers[index]}`)
if (index !== i) {
swap(numbers, index, i)
console.log(`swap ${index}: ${i}`)
console.log(numbers)
}
}
return numbers
}

sort([2, 55, 78, 99, 88, 654])

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const quickSort = arr => {
if (arr.length <= 1) {
return arr
}
let pivotIndex = Math.floor(arr.length / 2)
let pivot = arr.splice(pivotIndex, 1)[0]
let left = [],
right = []
for (let i = 0; i < arr.length; i++) {
if (arr[i] < pivot) {
left.push(arr[i])
} else {
right.push(arr[i])
}
}
return quickSort(left).concat([pivot], quickSort(right))
}

quickSort([24, 946, 9, 45, 6])

归并排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const mergeSort = arr => {
if (arr.length === 1) {
return arr
}
let left = arr.slice(0, Math.floor(arr.length / 2))
let right = arr.slice(Math.floor(arr.length / 2))
return merge(mergeSort(left), mergeSort(right))
}

const merge = (a, b) => {
if (a.length === 0) return b
if (b.length === 0) return a
return a[0] > b[0] ? [b[0]].concat(merge(a, b.slice(1))) : [a[0]].concat(merge(a.slice(1), b))
}

mergeSort([23, 8, 4, 99, 12])


计数排序

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
const countingSort = arr => {
let hashTable = {},
max = 0,
result = []
for (let i = 0; i < arr.length; i++) {
if (!(arr[i] in hashTable)) {
hashTable[arr[i]] = 1
} else {
hashTable[arr[i]] += 1
}
if (arr[i] > max) {
max = arr[i]
}
}
for (let j = 0; j <= max; j++) {
if (j in hashTable) {
for (let i = 0; i < hashTable[j]; i++) {
result.push(j)

}
}
}
return result
}
countingSort([2, 9, 5, 7])

时间复杂度对比

选择排序 O(n^2)

快速排序 O(n * log2 n)

归并排序 O(n * log2 n)

计数排序 O(n + max)

MVC是什么

MVC 是一种设计模式,他将应用分为三个部分:

M 是 Model,数据模型,负责数据相关的任务

V 是 View,视图,负责用户界面

C 是 Controller,控制器,负责监听用户事件,然后调用 M 和 V 更新数据和视图

伪代码实现 MVC

M

1
2
3
4
5
6
7
8
9
10
11
M = {
data: { 程序需要操作的数据或信息 },
create: { 增数据 },
delete: { 删数据 },
update(data) {
Object.assign(m.data, data) // 使用新数据替换旧数据
eventBus.trigger('m:upate') // eventBus 触发'm:update'信息, 通知 View 刷新
},
get:{ 获取数据 }
}

V

1
2
3
4
5
6
7
8
V = {
el: 需要刷新的元素,
html: `显示在页面上的内容`
init(){
v.el: 需要刷新的元素
},
render(){ 刷新页面 }
}

C

1
2
3
4
5
6
7
8
9
10
11
12
13
14
C = {
init(){
v.init() //初始化 V
v.render() // 第一次渲染页面
c.autoBindEvents() // 自动的事件绑定
eventBus.on('m:update',()=>{v.render()} // 当 enentsBus 触发 'm:update' 时 V 刷新
},
events:{事件以哈希表的方式记录存储},
method(){
data = 新数据
m.update(data)
},
autoBindEvents(){自动绑定事件}
}

mvc-demo

EventBus

EventBus 又称为事件总线,可以用来进行组件之间的监听和通信。

比如说当 Model 模块中的数据发生更新,触发了 EventBus 上的某个事件,而 Controller 恰好在监听这个事件,当这个事件触发时,Controller 就知道 Model 中的数据发生了更新了,从而做出一些反应

常用 API

  • EventBus.on() 监听事件
  • EventBus.trigger() 触发事件
  • EventBus.off() 解绑事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class EventBus {
constructor() {
this._eventBus = $(window)
}

on(eventName, fn) {
return this._eventBus.on(eventName, fn)
}

trigger(eventName, data) {
return this._eventBus.trigger(eventName, data)
}

off(eventName, fn) {
return this._eventBus.off(eventName, fn)
}
}

表驱动编程

《代码大全》对表驱动编程的描述:

表驱动方法是一种使你可以在表中查找信息,而不必用逻辑语句(if 或 case)来把他们找出来的方法。事实上,任何信息都可以通过表来挑选。在简单的情况下,逻辑语句往往更简单而且更直接。但随着逻辑链的复杂,表就变得越来越富于吸引力了。

表驱动编程的意义在于逻辑与数据的分离(类似于事件委托)

例如:if else

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
function translate(term) {
if (term === '1') {
return '一'
} else if (term === '2') {
return '二'
} else if (term === '3') {
return '三'
} else {
return '???'
}
}

// 如果想添加一个新的名词翻译,需要再添加一个if-else逻辑,例如:
function translate(term) {
if (term === '1') {
return '一'
} else if (term === '2') {
return '二'
} else if (term === '3') {
return '三'
} else if (term === '4') {
// 此处添加了一个新的名词翻译
return '四'
} else {
return '???'
}
}

改为表驱动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function translate(term) {
let terms = {
'1': '一',
'2': '二',
'3': '三'
}
return terms[term];
}

// 如果想添加一个新的名词翻译,只需要在 terms 中添加一个新的表项,不需要修改整个逻辑
function translate(term) {
let terms = {
'1': '一',
'2': '二',
'3': '三'
'4': '四' // 添加一个新的名词翻译
}
return terms[term];
}




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 示例二
c = {
events:{
'click #add1':'add',
'click #minus1':'minus',
'click #mul2':'mul',
'click #divide2':'div'
},
autoBindEvents(){
for(let key in c.events){
const value = c[c.events[key]]
const spaceIndex = key.indexOf(' ')
const part1 = key.slice(0, spaceIndex)
const part2 = key.slice(spaceIndex + 1)
v.el.on(part1,part2,value)
}
}
}

转自表驱动编程

模块化

JavaScript modules 模块 MDN

模块就是实现特定功能的一组方法,而模块化是将模块的代码创造自己的作用域,只向外部暴露公开的方法和变量,而这些方法之间高度解耦。

一个应用的不同的功能分离成几个模块,需要什么功能就加载相应的模块。

一个小程序,输出各种形状的面积和周长

1
2
3
4
5
6
7
8
9
10
// 正方形 边长、面积、周长
let square = {
width: 5,
getArea() {
return this.width * this.width;
},
getLength() {
return this.wdith * 4;
}
}

二,生成12个正方形

1
2
3
4
5
6
7
8
9
10
11
12
let squareList = []
for(let i = 0; i<12; i++){
squareList[i] = {
width: 5,
getArea(){
return this.width * this.width
},
getLength(){
return this.width * 4
}
}
}

三,width 是 5 和 6相间

1
2
3
4
5
6
7
8
9
10
11
12
13
let squareList = []
let widthList = [5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6]
for(let i = 0; i<12; i++){
squareList[i] = {
width: widthList[i],
getArea(){
return this.width * this.width;
},
getLength(){
return this.widht * 4;
}
}
}

三,垃圾代码,浪费太多内存,画内存图

squareMemory

四,借助原型,将12个对象的共有属性放到原型里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let squareList = []
let widthList = [5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6]
let squarePrototype = {
getArea() {
return this.width * this.width;
},
getLength() {
return this.width * 4;
}
}
for(let i = 0; i<12; i++){
squareList[i] = Object.create(squarePrototype);
squareList[i].width = widthList[i];
}

// 还是垃圾代码,创建 square 的代码太分散了

五,把代码抽离到一个函数里,然后调用函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let squareList = []
let widthList = [5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6]
function createSquare(width){ // 此函数为叫做构造函数
let obj = Object.create(squarePrototype);
// 以 squarePrototype 为原型创建空对象
obj.width = width;
return obj;
}
let squarePrototype = {
getArea() {
return this.width * this.width;
},
getLength(){
return this.width * 4;
}
}
for(let i = 0; i<12; i++){
squareList[i] = createSquare(widthList[i]);
}

六,函数和原型的结合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 这段代码 几乎完美
let squareList = []
let widthList = [5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6]
function createSquare(width){
let obj = Object.create(createSquare.squarePrototype);
obj.width = width;
return obj
}
createSquare.squarePrototype = { //把原型放到函数上
getArea(){
return this.width * this.width;
},
getLength(){
return this.width * 4;
},
constructor: createSquare // 互相引用方便通过原型找到构造函数
}
for(let i = 0; i<12; i++){
squareList[i] = createSquare(widthList[i]);
console.log(squareList[i].constructor);
}

七,函数和原型(重写)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let squareList = []
let widthList = [5, 6, 5, 6, 5, 6, 5, 6, 5, 6, 5, 6]

function Square(width){ // 构造函数
this.width = width;
}
Square.prototype.getArea = function(){
return this.width * this.width;
}
Square.prototype.getLength = function(){
return this.width * 4;
}

for(let i = 0; i>12; i++){
squareList[i] = new Square(widthList[i]);
console.log(squareList[i].constructor);
}
// 完美

代码六七对比

Snipaste_2021-08-31_12-16-01

总结

  • new X()
    • 自动创建空对象
    • 自动为空对象关联原型,原型地址指定为 X.prototype
    • 自动将空对象作为 this 关键字运行构造函数
    • 自动 return this
  • 构造函数 X
    • X 函数本身负责给对象本身添加属性
    • X.prototype 对想负责保存对象那个的公用属性

题外话 代码规范:

  • 大小写

    • 所有构造函数(专门用于创建对象的函数)首字母大写
    • 所有被构造出来的对象,首字母小写
  • 词性

    • new 后面的函数,使用名词形式
    • 如 new Person()、new Object()
    • 其他函数,一般使用动词开头
    • 如 createSquare(5)、createElement(‘div’)

原型公式:**对象._proto_ === 其构造函数.protorype**

Square 最终版(存疑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Square(width){
this.width = width;
}

Square.prototype.getArea = function(){
return this.width * this.width;
}
Square.prototype.getLenth = function(){
return this.width * 4;
}

let square = new Square(5);
square.width;
square.getArea();
square.getLength();

练习

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
// 圆形
function Circle(radius){
this.radius = radius;
}
Circle.prototype.getArea = function(){
return this.radius * 2 * Math.PI;
}
Circle.prototype.getLength = function(){
return Math.pow(this.radius, 2) * Math.PI;
}
let c1 = new Circle(10);
c1.getArea();
c1.getLength();

// 长方形
function Rect(width, height) {
this.width = width;
this.height = height
}
Rect.prototype.getArea = function(){
return this.width * this.height
}
Rect.prototype.getLength = function(){
return (this.width + this.height) * 2;
}
let r1 = new Rect(2, 3)
r1.getArea()
r1.getLength()

对象分类

分类

  • 理由一
    • 有很多对象拥有一样的属性和行为
    • 需要把他们分为同一类
    • 如 square 和 square1
    • 这样创建类似对象的时候就很方便
  • 理由二
    • 但是还有很多对象拥有其他的属性和行为
    • 所以就需要不同的分类
    • 比如 Square / Circle / Rect 就是不同的分类
    • Array / Function 也是不同的分类
    • 而 Object 创建出来的对象,是最没有特点的对象

类型和类

类型

类型 JS 数据的分类,有七种

四基两空一对象,String,number,bool,symbol,null,undefined,Object

类是针对对象的分类, 有无数种

常见的有 Array、Function、Date、RegExp 等

数组对象

定义一个数组

1
2
3
let arr = [1, 2, 3]
let arr = new Array(1, 2, 3) //元素为 1,2,3
let arr = new Array(3) //长度为3

数组对象自身的属性

  • ‘0’ / ‘1’ / ‘2’ / ‘length’

  • 注意,属性名没有数字,只有字符串

数组对象的共用属性

‘push’ / ‘pop’ / ‘shift’ / ‘unshift’ / ‘join’ 等

函数对象

定义一个函数

1
2
3
4
function fn(x, y){return x + y;}
let fn2 = function fn(x, y) {return x + y}
let fn = (x, y) => x+y
let fn = new Function('x', 'y','return x+y')

函数对象自身属性

‘name’ / ‘length’

函数对象共用属性

‘call’ / ‘apply’ / ‘bind’ 等

JS 终极一问

window 是谁构造的

Window

可以通过 constructor 属性看出构造者

验证:window.constructor

window._proto_ === Windows.prototype

window.Object 是谁构造的

window.Funcion

因为所有函数都是 window.Function 构造的

验证:window.Object.constructor === window.Function

window.Function 是谁构造的

window.Function

因为所有函数都是 window.Function 构造的

自己构造的自己?不是这样的,这是『上帝』的安排

浏览器构造了 Function,然后指定它的构造者是自己

验证:window.Function.constructor === window.Function

class

类是用于创建对象的模板。他们用代码封装数据以处理该数据。 ES6新语法

学习class对象初始化解构赋值

constructor 是一种用于创建和初始化 class 创建的对象的特殊方法.

1
2
3
4
5
6
7
8
9
10
11
class Square{
constructor(width){
this.width = width;
}
getArea(){
return this.width * this.width;
}
getLength(){
return this.width * 4;
}
}

class 引入更多概念

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Square{
static x = 1; // 静态
width = 0;
constructor(width){
this.width = width;
}
getArea(){
return this.width * this.width;
}
getLength(){
return this.width * 4;
}
get area2(){ // 只读属性
return this.width * this.width;
}
}

class 重写

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
// 重写 Circle
class Circle{
constructor(radius){
this.radius = radius;
}
getArea(){
return Math.pow(this.radius,2) * Math.PI;
}
getLength(){
return this.radius * 2 * Math.PI;
}
}
let c2 = new Circle(5);
c2.radius;
c2.getArea();
c2.getLength();

// 重写 Rect
class Rect{
constructor(width, height){
this.width = width;
this.height = height;
}
getArea(){
return this.width * this.height;
}
getLength(){
return (this.width + this.height) * 2;
}
}
let r2 = new Rect(3, 4);
r2.width;
r2.height;
r2.getArea();
r2.getLength();

class 中两种函数写法的区别

这两种写法的意思完全不一样:

语法1:

1
2
3
4
5
6
7
8
9
class Person{
sayHi(name){}
// 等价于
sayHi: function(name){}
// 注意,一般我们不在这个语法里使用箭头函数
}
//等价于
function Person(){}
Person.prototype.sayHi = function(name){}

语法2:注意冒号变成了等于号

1
2
3
4
5
6
7
8
class Person{
sayHi = (name)=>{} // 注意,一般我们不在这个语法里使用普通函数,多用箭头函数
}
// 等价于
function Person(){
this.sayHi = (name)=>{}
}

推荐阅读

  1. 方应杭:你可以不会 class,但是一定要学会 prototype
  2. 方应杭:JS 的 new 到底是干什么的?
  3. 方应杭:JS 中 proto 和 prototype 存在的意义是什么?
  4. 饥人谷整理的 ES6 所有新特性

简介

jQuery 是目前使用最广泛的 JavaScript 函数库

对于网页开发者来说,学会 jQuery 是必要的。因为它让你了解业界最通用的技术,为将来学习更高级的库打下基础,并且确实可以很轻松地做出许多复杂的效果。

jQuery 是一个高效、精简并且功能丰富的 JavaScript 工具库。它提供的 API 易于使用且兼容众多浏览器,这让诸如 HTML 文档遍历和操作、事件处理、动画和 Ajax 操作更加简单。

jQuery 的基本设计思想就是,选择某个网页元素,然后对其进行某种操作

jQuery 的实质就是一个封装的 DOM 库

仿 jQuery 手写了一些简单的 api,有兴趣可以参考一下 简易jQuery

选择网页元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//jQuery中:jQuery() = $()
//将一个选择表达式,放进构造函数$()

//选择表达式可以是CSS选择器
$(document) //选择整个文档对象
$('#myId') //选择ID为myId的网页元素
$('div.myClass') // 选择class为myClass的div元素
$('input[name=first]') // 选择name属性等于first的input元素

//也可以是jQuery特有的表达式
$('a:first') //选择网页中第一个a元素
$('tr:odd') //选择表格的奇数行
$('#myForm :input') // 选择表单中的input元素
$('div:visible') //选择可见的div元素
$('div:gt(2)') // 选择所有的div元素,除了前三个
$('div:animated') // 选择当前处于动画状态的div元素

精确选择范围

思想:提供各种强大的过滤器,对结果集进行筛选,缩小选择结果

1
2
3
4
5
6
7
8
9
10
11
$('div').has('p'); // 选择包含p元素的div元素
$('div').not('.myClass'); //选择class不等于myClass的div元素
$('div').filter('.myClass'); //选择class等于myClass的div元素
$('div').first(); //选择第1个div元素

//也可以从当前元素出发,移动到附近的相关元素
$('div').next('p'); //选择div元素后面的第一个p元素
$('div').parent(); //选择div元素的父元素
$('div').closest('form'); //选择离div最近的那个form父元素
$('div').children(); //选择div的所有子元素
$('div').siblings(); //选择div的同级元素

复制、删除、创建元素

  • 复制元素使用 .clone()
  • 删除元素使用 .remove().detach()

两者区别:前者不保留被删除元素的事件,后者保留,有利于重新插入文档时使用

  • 清空元素内容(但是不删除该元素)使用 .empty()
  • 创建元素只要把新元素直接传入jQuery的构造函数就行了
1
2
3
$('<p>Hello</p>');
$('<li class="new">new list item</li>');
$('ul').append('<li>list item</li>');

链式操作

思想:最终选中网页元素以后,可以对它进行一系列操作,并且所有操作可以连接在一起,以链条的形式写出来

这样以点连起来的连续调用就称为链式调用,它的设计原理是把jQuery中的函数返回为这个对象,然后这个对象又包含jQuery的所有方法,这样就可以实现链式调用,十分方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$('div').find('h3').eq(2).html('Hello');

//分解开,可以这么理解
$('div') //找到div元素
   .find('h3') //选择其中的h3元素
   .eq(2) //选择第3个h3元素
   .html('Hello'); //将它的内容改为Hello

//jQuery还提供了.end()方法,使得结果可以后退一步
$('div')
   .find('h3')
   .eq(2)
   .html('Hello')
   .end() //退回到选中所有的h3元素的那一步
   .eq(0) //选中第一个h3元素
   .html('World'); //将它的内容改为World

移动元素

思想:jQuery提供两组方法,来操作元素在网页中的位置移动。一组方法是直接移动该元素,另一组方法是移动其他元素,使得目标元素达到我们想要的位置

1
2
3
4
5
6
7
//第一种方法是使用.insertAfter()
//把div元素移动p元素后面
$('div').insertAfter($('p'));

//第二种方法是使用.after()
//把p元素加到div元素前面
$('p').after($('div'));

二者区别:返回的元素不一样。第一种方法返回div元素,第二种方法返回p元素。

类似操作方法共4对

1
2
3
4
 .insertAfter()和.after() //在现存元素的外部,从后面插入元素
 .insertBefore()和.before() //在现存元素的外部,从前面插入元素
 .appendTo()和.append() //在现存元素的内部,从后面插入元素
 .prependTo()和.prepend() //在现存元素的内部,从前面插入元素

重载

jQuery中,构造函数$()支持多种参数

  • .attr(name,value)和.attr(name):两个参数是设置属性,一个参数是获取属性值
  • .text()和.text(string):无参数是获取文本,一个参数是写入文本
  • .html()和.html(string):无参数是获取html元素,一个参数是写入html元素

结合下面这段代码,参数可以是选择器、数组或一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
window.jQuery = function(selectorOrArrayOrTemplate){
let elements
if(typeof selectorOrArrayOrTemplate === 'string'){
if(selectorOrArrayOrTemplate[0] === '<'){
// 创建 div
elements=[createElement(selectorOrArrayOrTemplate)]
}else{
// 查找 div
elements = document.querySelectorAll(selectorOrArrayOrTemplate)
}
}else if(selectorOrArrayOrTemplate instanceof Array){
elements = selectorOrArrayOrTemplate
}

//举例
$('div')
$('<div>hello</div>')


getter/setter

思想:使用同一个函数,来完成取值(getter)和赋值(setter),到底是取值还是赋值,由函数的参数决定。

1
2
$('h1').html(); //html()没有参数,表示取出h1的值
$('h1').html('Hello'); //html()有参数Hello,表示对h1进行赋值

常见的取值和赋值函数

1
2
3
4
5
6
.html() 取出或设置html内容
.text() 取出或设置text内容
.attr() 取出或设置某个属性的值
.width() 取出或设置某个元素的宽度
.height() 取出或设置某个元素的高度
.val() 取出某个表单元素的值

如果结果集包含多个元素,那么赋值的时候,将对其中所有的元素赋值。
取值的时候,则是只取出第一个元素的值(.text()例外,它取出所有元素的text内容)

适配器

jQuery针对不同浏览器使用不同代码,也就是兼容各个浏览器。

用闭包隐藏细节

思想:浏览器必须通过内部的函数来操作外部参数,如用get()或find()来操作elements,可以隐藏细节。

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
window.$ = window.jQuery = function(selectorOrArrayOrTemplate) {
let elements;
if (typeof selectorOrArrayOrTemplate === "string") {
if (selectorOrArrayOrTemplate[0] === "<") {
// 创建 div
elements = [createElement(selectorOrArrayOrTemplate)];
} else {
// 查找 div
elements = document.querySelectorAll(selectorOrArrayOrTemplate);
}
} else if (selectorOrArrayOrTemplate instanceof Array) {
elements = selectorOrArrayOrTemplate;
}

function createElement(string) {
const container = document.createElement("template");
container.innerHTML = string.trim();
return container.content.firstChild;
}

jQuery.fn = jQuery.prototype = {
constructor: jQuery,
get(index) {
return this.elements[index];
},
find(selector) {
let array = [];
for (let i = 0; i < this.elements.length; i++) {
const elements2 = Array.from(this.elements[i].querySelectorAll(selector));
array = array.concat(this.elements2);
}
array.oldApi = this; // this 就是 旧 api
return jQuery(array);
},
}
}

参考自:

jQuery设计思想

定义函数

定义一个函数

  • 具名函数

    1
    2
    3
    function 函数名(形式参数1, 形式参数2){
    return 返回值
    }
  • 匿名函数, 具名函数去掉函数名就是匿名函数

    1
    2
    3
    4
    let a = function(x, y){
    return x + y
    }
    // 也叫函数表达式
  • 箭头函数

    1
    2
    3
    4
    5
    let f1 = x => x * x
    let f2 = (x, y) => x + y // 圆括号不能省
    let f3 = (x, y) => { return x - y} // 花括号不能省
    let f4 = (x, y) => ({name: x, age: y})
    // 直接返回对象 需要加个圆括号
  • 构造函数

    1
    2
    3
    4
    let f = new Function('x', 'y', 'return x + y')
    // 基本没人用, 但是能让你知道函数是谁构造的
    // 所有函数都是 Function 构造的
    // 包括 Object、Array、Function

函数的要素

每个函数都拥有这些

  • 调用时机
  • 作用域
  • 闭包
  • 形式参数
  • 返回值
  • 调用栈
  • 函数提升
  • arguments(除了箭头函数)
  • this(除了箭头函数)

调用时机

时机不同, 结果不同

1
2
3
4
5
6
7
8
9
let a = 1;
function fn(){
setTimeout( () => {
console.log(a)
}, 0)
}
fn()
a = 2
// 打印结果为 2
1
2
3
4
5
6
7
let i = 0;
for(i = 0; i < 6; i++){
setTimeoug( () => {
console.log(i)
}, 0)
}
// 打印结果为 6个6
1
2
3
4
5
6
7
8
for(let i = 0; i < 6; i++){
setTimeoug( () => {
console.log(i)
}, 0)
}
// 打印结果为 0, 1, 2, 3, 4, 5
// 因为 JS 在 for 和 let 一起用的时候会加东西
// 每次循环会多创建一个 i

作用域

每个函数都会创建一个作用域

深入理解 JS 作用域和作用域链

1
2
3
4
5
// 例1
function fn(){ let a = 1; }
console.log(a) // a 不存在
// 问: 是不是因为 fn 没执行导致
// 答: 就算 fn 执行了,也访问不到作用域里面的 a
1
// 例2function f1(){    let a = 1;        function f2(){        let a = 2;         console.log(a);    }        console.log(a);    a = 3;    f2();    console.log(a)}f1()// 打印结果为 /*  1 	2	3*/
1
// 例3function f1(){    let a = 1;    function f2(){        let a = 2;        function f3(){            console.log(a)        }        a = 22;        f3();    }    console.log(a);    a = 100;    f2();}f1();/* 打印机结果为122*/

如果多个作用域有同名变量 a

那么查找 a 的声明式, 就向上取最近的作用域, 简称[就近原则]

查找 a 的过程与函数执行无关

但 a 的值与函数执行有关

闭包

JS中的闭包是什么

1
function f1(){    let a = 1;    function f2(){        //        let a = 2;        function f3(){            console.log(a)        }//               /*		如果一个函数用到外部的变量		那么这个函数加这个变量		就叫做 闭包		左边的 a 和 f3 组成了 闭包		*/                a = 22;        f3();    }    console.log(a);    a = 100;    f2();}f1();

形式参数

形式参数意思式非实际参数

1
function add (x, y){    return x + y;}// 其中 x 和 y 就是形参,因为并不是时机的参数add(1, 2);// 调用 add 时,1 和 2 是实际参数,会被复制给 x y// 上面代码近似等于下面代码function add(){    var x = arguments[0]    var y = arguments[1]    return x + y}

返回值

每个函数都有返回值

函数执行完了后才会返回

只有函数有返回值

1
function hi() { console.log('hi'); }hi();// 没写 return, 所以返回值是 undefinedfunction hi() { return console.log('hi'); }hi()// 返回值为 console.log('hi') 的值,即 undefined

调用栈

什么是调用栈

JS 引擎在调用一个函数前

需要把函数所在的环境 push 到一个数组里

这个数组叫做调用栈

等函数执行完了, 就会把环境弹 (pop) 出来

然后 return 到之前的环境, 继续执行后续代码

Snipaste_2021-09-02_17-04-35
调用栈 图示

爆栈, 如果调用栈中压入的帧过多, 程序就会奔溃

递归函数

1
// 阶乘function f(n) {    return n !== 1 ? n * f(n - 1) : 1}// 理解递归f(4)= 4 * f(3)= 4 * (3 * f(2))= 4 * (3 * (2 * f(1)))= 4 * (3 * (1))= 4 * (6)= 24// 先递进, 后回归
Snipaste_2021-09-02_17-36-31
递归函数调用栈 图示

调用栈最长有多少

1
// 测试调用栈长度function computeMaxCallStackSize() {    try {        return 1 + computeMaxCallStackSize();    } catch(e) {        // 报错说明 stack overflow 了        return 1    }}/*chrom 11409firefox 24740node 12536*/

函数提升

1
function fn(){}// 不管把具名函数声明在哪里, 它都会跑到第一行

JS 变量提升和函数提升

前端面试必考-JS 变量提升和函数提升详解

arguments 和 this

每个函数都有 arguments 和 this ,除了箭头函数

1
function fn(){    console.log(arguments);    console.log(this)}fn()// arguments 是包含所有参数的 伪数组// 如果不给任何条件 this 默认指向 window
1
function fn(){    console.log(this)}fn()fn.call(1) // 打印出的 数字 1 被自动转化成对象 1function fn(){    'use strict'    console.log(this)}fn.call(1) // 打印出 数字 1function fn(){  console.log(this);  console.log(arguments)}// 传入的第一个参数是 this, 其余的是 argumentsfn.call(1, 2, 4)/*打印结果Number (1)Arguments{0: 2, 1: 4 ...}*/
Snipaste_2021-09-02_21-14-03
arguments 和 this

this 是隐藏参数

arguments 是普通参数

this 是参数(个人结论)

假如没有 this

1
let person = {    name: 'frank',    sayHi(){        console.log('hello, i am ' + person.name);    }}person.sayHi()/* 分析可以用直接保存了对象地址的 变量 获取 'name'这种办法简称为 引用*/

问题一

1
let sayHi = function(){    console.log('hello, i am ' + /* person*/.name)}let person = {    name: 'frank',    'sayHi': sayHi.}/*分析person 如果改名,sayHi 函数就挂了sayHi 函数甚至有可能在另一个文件里所以我们不希望 sayHi 函数里出现 person 引用*/

问题二

1
class Perosn {    constructot(name){        this.name = name        // 这里的 this 是 new 强制指定的    }    sayHi(){        console.log(/*????*/)    }}/*分析这里只有类,还没创建对象,故不可能获取对象的引用那么如何拿到对象的 name ?*//*需要一种办法拿到对象这样才能获取对象的 name 属性*/

一种土方法,用参数

1
// 对象let person = {    name: 'frank',    sayHi(p) {        console.log('hello, i am ' + p.name)    }}person.sayHi(person)// 类class Person {    constructor(name){ this.name = name }    sayHi(p) {        console.log('hello, i am ' + p.name)    }}let person = new Person('frank')person.sayHi(person)

JS 在每个函数里加了 this

1
// 用 this 获取那个对象let person = {    name: 'frank',    sayHi(/*this*/){        console.log('hello, i am ' + this.name)    }}person.sayHi()/*person.sahHi()相当于 person.sayHi(person)然后 person 被传给 this 了 (person 是个地址)这样每个函数都能用 this 获取一个未知对象的引用了*/// person.sayHi() 会隐式地把 person 作为 this 传给 sayHi// 方便 sayHi 获取 person 对应的对象

总结

我们想让函数获取对象的引用

但是并不想通过变量名做到

Python 通过额外的 self 参数做到

JS 通过额外的 this 做到:

person.sayHi() 会把person 自动传给 sayHi, sayHi 可以通过 this 引用 person

其他

  • 注意 person.sayHi 和 person.sayHi() 的区别
  • 注意 person.sayHi() 的短句 (person.sayHi)()

call 指定 this

1
// 哪个对let person = {    name: 'frank',    sayHi(/*this*/){        console.log('hello, i am ' + this.name)    }}person.sayHi()Person.sayHi(person)// 省略形式的反而是对的// 两种调用方式person.sayHi()// 会自动把 person 传到函数里, 作为 thisperson.sayHi.call(person)person.sayHi.call({name: 'evan'})// 需要手 动把 person 传到函数里,作为 this// 推荐使用
1
// 例1function add(x, y) {    return x + y}add.call(undefined, 1, 2) // 3/*为什么要多写一个 undefined因为第一个参数要作为 this但是代码里没有用 this所以只能用 undefined 占位其实用 null 也可以*/
1
// 例2Array.prototype.forEach2 = function(fn){    for(let i = 0; i < this.length; i++){        fn(this[i], i, this)    }}let arr1 = [2, 4, 5]// 两种调用方式arr1.forEach2.call(arr1, (item) = > console.log(item))arr1.forEach2((item) = > console.log(item))/* this 是什么由于大家使用 forEach2 的时候总是会用 arr.forEach2所以 arr1 就被自动传给 foreEach2 了*//* this 一定是数组吗不一定, 比如Array.prototype.forEach2.call({0: 'a', 1: 'b',length: 2})*/

this 的两种使用方法

  • 隐式传递

    1
    fn(1, 2) // 等价于 fn.call(undefined, 1, 2)obj.child.fn(1) // 等价于 obj.child.fn.call(obj.child, 1)
  • 显示传递

    1
    fn.call(undefined, 1, 2)fn.apply(undefined, [1, 2]) // 数组

**绑定 this **

1
// 使用 .bind 可以让 this 不被改变function f1(p1, p2){    console.log(this, p1, p2)}let f2 = f1.bind({name: 'frank'})// 那么 f2 就是 f1 绑定 this 之后的新函数f2() // 等价于 f1.call({name: 'frank'})

快速理解 JS 中 this 的用法与陷阱

箭头函数

没有 arguments 和 this

里面的 this 就是外面的 this

1
console.log(this) // windowlet fn = () => console.log(this)fn() // window// 就算加了 call 也没用fn.call({name: 'frank'}) // window

立即执行函数

Snipaste_2021-09-03_23-05-46
立即执行函数

推荐 JS 中的立即执行函数

跨域关键知识:

  • 同源策略。浏览器故意设计的一个功能限制
  • CORS。突破浏览器限制的一个方法
  • JSONP。IE 时代的妥协

同源

源 = 协议 + 域名 + 端口号

window.originlocation.origin 可以得到当前源。

如果两个 url 的协议、域名、端口号完全一致,那么这两个 url 就是同源。

例:https://qq.comhttps://www.baidu.com 不同源

https://baidu.comhttps:www.baidu.com 同源

完全一致才算同源

下表给出了与 URL http://store.company.com/dir/page.html 的源进行对比的示例:

URL 结果 原因
http://store.company.com/dir2/other.html 同源 只有路径不同
http://store.company.com/dir/inner/another.html 同源 只有路径不同
https://store.company.com/secure.html 失败 协议不同
http://store.company.com:81/dir/etc.html 失败 端口不同 ( http:// 默认端口是80)
http://news.company.com/dir/other.html 失败 主机不同

同源策略

Ajax 最大的限制是同源策略(Same-origin policy),它限制了不同源之间的交互,一个源的文档或脚本不能与另一个源的资源进行交互。浏览器的同源策略 MDN

  • 浏览器规定

    如果 JS 运行在源 A 里,那么就只能获取源 A 的数据

    不能获取源 B 的数据,即==不允许跨域==

  • 例如(省略 http 协议)

    假设 frank.com/index.html 引用了 cdn.com/1.js

    那么就说 1.js 运行在源 frank.com 里

    注意 这跟 cdn.com 没有关系,虽然 1.js 从它哪下载

    所以 1.js 就只能获取 frank.com 的数据

    不能获取 1.frank.com 或者 qq.com 的数据

  • 这是浏览器的功能

    浏览器故意要这样设计的

    目的:==保护用户隐私==

如果没有同源策略

以 qq 空间为例

源为 https://user.qzone.qq.com,假设,当前用户已登录(cookie),假设 AJAX 请求 /friends.json 可获取到用户好友列表。

黑客来了,假设有人给你分享 https://qzone-qq.com 给你,实际上是个钓鱼网站,你点开后,这个网页会请求你的好友列表 https://user.qzone.qq.com/friends.json。这样好友列表就能被黑客访问到。

问题根源:

  • 无法区分发送者

    qq 空间页面的 JS 和黑客网页里的 JS 发送的请求几乎没有区别(referrer 有区别)

    如果后台开发者没有检查 referrer,那么就完全没有区别

    所以,没有同源策略,任何页面都能偷 qq 空间的数据

  • 那检查 referrer 不就好了

    安全原则:安全链条的强度取决于最弱的一环

    万一这个网站的后端开发工程师就是没有检查 referrer

    所以浏览器应该主动预防这种偷数据的行为

    总之,浏览器为了用户隐私,设置了严格的同源策略

演示

  1. 创建目录

    qq-com 里新建 server.js,用来模拟 qq空间

    frank-com 里新建 server.js,用来模拟黑客网站

  2. qq-com

    public 目录下新建 index.html 首页

    qq.js 是 JS 脚本文件

    friends.json 是模拟的好友数据

    端口监听为 8888,访问 http://127.0.0.1:8888

  3. hacker-com

    public 目录下新建 index.html 首页

    frank.js 是 JS 脚本文件

    端口监听为 9999,范问 http://127.0.0.1:9999

跨域 AJAX

  • 正常使用 AJAX

    在 qq.com:8888 里运行的 JS 可以访问 /friends.json

    Snipaste_2021-09-27_01-44-31
    能够访问
  • 黑客偷数据

    在 hacker.com:9999 里运行的 JS不能访问

    浏览器需要==CORS==

Snipaste_2021-09-27_01-44-56
不能访问
  • 提问

    黑客的请求成功了没:

    答:成功了,因为 qq.com 后台有 log。

    黑客拿到响应了没与?

    答:没有,因为浏览器不给数据。

Snipaste_2021-09-27_01-53-35

如何跨域

CORS

CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing)。

  • 问题根源

    浏览器默认不同源之间不能互相访问数据

    但是 qq.com 和 hacker.com 都是自己的网站,需要互相访问

  • 用 CORS

    浏览器说,如果要共享数据,需要提前声明!

    qq.com 在响应头里写 hacker.com 可以访问

    语法:Access-Control-Allow-Origin: http://hacker.com:9999

  • MDN 文档

CORS 不兼容 ie 6789

JSONP

什么是 JSONP?

跨域时,由于当前浏览器不支持 CORS 或因为某些条件不支持 CORS,我们必须使用另外一种方式来跨域。

于是,请求一个 JS 文件,这个 JS 文件会执行事先定义好的回调,这个回调里就有我们需要的数据。

优点:

  • 支持 IE

缺点:

  • 由于是 script 标签,获取不到响应状态
  • 不支持 POST

演示:hacker.com 访问 qq.com

  • qq.com 将数据写到 /friends.js
  • frank.com 用 script 标签引用 /friends.js
  • hacker.com 执行 事先定义好的 window.xxx 函数
  • /friends.js 执行 window.xxx({friend: […]})
  • 然后 hacker.com 就通过 window.xxx 获取到数据了
  • window.xxx 就是一个回调!!

JSONP 的实现原理演示:

hacker-com 里的 hacker.js

1
2
3
4
5
6
window.xxx = (data) => {
console.log(data)
}
const script = document.createElement('script');
script.src = 'http://qq.com:8888/friends.js';
document.body.appendChild(script);

qq-com public目录下新建 friends.js 内容为 window.xxx ( {{ data }} )

qq-com 的 server.js 添加如下路由

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
else if (path === '/friends.js') {
if (request.headers['referer'].indexOf('http://hacker.com:9999') === 0) {
response.statusCode = 200
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
const string = fs.readFileSync('./public/friends.js').toString()
const data = fs.readFileSync('./public/friends.json').toString()
response.write(string.replace('{{ data }}', data))
response.end()
} else {
response.statusCode = 404;
response.end();
}
}
// 控制台 window.xxx
// hacker 拿到了 friends.js 的数据

优化:

window.xxx 能不能改其他名字?

其实名字不重要,只要 hacker.com 定义的函数名和 qq.com/friends.js 执行的函数名是同一个即可。

将名字穿给 /friends.js

1
2
3
4
5
6
7
8
9
10
11
12
// hacker.js
const random = `hackerJSONPCallback` + Math.random();
console.log(random);
window[random] = (data) => {
console.log(data)
}
const script = document.createElement('script');
script.src = `http://qq.com:8888/friends.js?functionName=${random}`;
script.onload = () => {
script.remove();
}
document.body.appendChild(script);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// server.js
else if (path === '/friends.js') {
if (request.headers['referer'].indexOf('http://hacker.com:9999') === 0) {
response.statusCode = 200
console.log(query.functionName);
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
const string = fs.readFileSync('./public/friends.js').toString()
const data = fs.readFileSync('./public/friends.json').toString()
response.write(string.replace('{{ data }}', data).replace(`{{ xxx }}`, query.functionName))
response.end()
} else {
response.statusCode = 404;
response.end();
}
}
1
2
// friends.js
window[`{{ xxx }}`]( {{ data }} )

进一步优化

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
// hacker.js
// 封装 JSONP
function jsonp(url) {
return new Promise((resolve, reject) => {
const random = `hackerJSONPCallback` + Math.random();
console.log(random);
window[random] = (data) => {
resolve(data)
};
const script = document.createElement('script');
script.src = `${url}?callback=${random}`;
script.onload = () => {
script.remove();
};
script.onerror = () => {
reject();
}
document.body.appendChild(script);
});
}

jsonp('http://qq.com:8888/friends.js')
.then((data) => {
console.log(data);
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// server.js
else if (path === '/friends.js') {
if (request.headers['referer'].indexOf('http://hacker.com:9999') === 0) {
response.statusCode = 200
console.log(query.functionName);
response.setHeader('Content-Type', 'text/javascript;charset=utf-8')
const string = `window['{{ xxx }}']( {{ data }} )`
const data = fs.readFileSync('./public/friends.json').toString()
response.write(string.replace('{{ data }}', data).replace(`{{ xxx }}`, query.callback))
response.end()
} else {
response.statusCode = 404;
response.end();
}
}

JSONP 的本质是前后端的协作,即前端把想要的资源以及后续的处理都告诉后台,后台封装好返回给前端执行。

优质博客:

不要再问我跨域的问题了

同源策略于JS跨域(JSONP,CORD)

思考

代码为什么会打印 6 个 6

1
2
3
4
5
6
let i = 0;
for(i = 0; i < 6; i++) {
setTimeout( () => {
console.log(i)
}, 0);
};

分析

在上面代码中,for 循环是同步代码,setTimeout 是异步代码,JS 按照从上到下的执行顺序执行同步代码,而异步代码被插入到任务队列中。

当执行完同步代码(for 循环),JS 会去执行异步代码(setTimeout)。

在每次 for 循环中,都将异步代码(setTimeout)放入任务队列中,所以任务队列中有 6 个 setTimeout 即有 6 个 console.log(i)

在每次 for 循环中将 setTimeout 里的代码 console.log(i) 放入任务队列时,i 的值是不一样的,当 JS 引擎开始执行任务队列中代码时,会在当前作用域中找变量 i ,但当前 for 循环的作用域中没有对变量 i 的进行定义,这个时候会在创造该函数的作用域中寻找 i,找到的是 let i,这时的 i 时全局变量,并且值已经确定为 6。所以打印出 6 个 6。

执行流程:

for(i=0) ==> for(i=1) ==> for(i=2) ==> for(i=3) ==> for(i=4) ==> for(i=5) ==> for(i=6) ==> console.log(6)x6

解决方法一 let

1
2
3
4
5
for( let i = 0; i < 6; i++ ){
setTimeout( ()=> {
console.log(i);
}, 0);
}

let 的作用域是块作用域,能作用到 for 循环的子块中。

let 的作用于是块作用域,所以 setTimeout 被放到 任务队列的同时,let 定义的 i 值 也会跟随 setTimeout 进入队列。所以每次循环后队列里的 setTimeout 里的 i 值是不一样的。而 var 定义的 i 是无法进入的。(浅显易懂)

for 循环头部的 let 不仅将 i 绑定到 for 循环中,事实上它将其重新绑定到循环体的每一次迭代中,确保上一次迭代结束的值被重新赋值。setTimeout 里面的函数属于一个新的域,通过 var 定义的变量或全局变量是无法传入到这个函数执行,通过使用 let 来声明块变量能作用于这个块,所以箭头函数就能使用 i 这个变量,所以每次的 i 值不一样。

解决方法二 使用立即执行函数,即闭包

1
2
3
4
5
6
7
8
let i = 0;
for( i = 0; i < 6; i++){
(function (j){
setTimeout( () => {
console.log(j);
}, 0);
})(i);
}

因为 setTimeout 是异步执行,所以让它立即执行就可以了。

通过闭包,将 i 的变量驻留在内存中,当输出 j 时,引用的是外部函数的变量值 i,i 的值是根据循环来的,执行 setTimeout 时已经确定了里面的的输出了。

解决方法三 setTimeout 第三个参数

1
2
3
4
5
6
7
let i = 0;
for (i = 0; i < 6; i++){
setTimeout( (i) => {
console.log(i);
}, 0, i);
}
// 将每次的 i 值传入作用域。

解决方法四 try catch

1
2
3
4
5
6
7
8
9
10
11
let i = 0;
for(i = 0; i < 6; i++){
try{
throw i
}catch(i){
setTimeout( () => {
console.log(i)
}, 0);
}
}
// 将 i 作为异常抛出,传递给 setTimeout

JS执行机制

首先,JS是单线程环境,代码从上到下依次执行。这种执行方这也被称作是“同步执行”。(同一时间 JS 只能执行一段代码,如果这段代码要执行很长时间,那么之后的代码只能尽情地等待它执行完才能执行)。

但 JS 中引进了异步机制。于是,所有任务可以分成两种,一种是同步任务(synchronous),另一种是异步任务(asynchronous)。

同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入”任务队列”(task queue)的任务,只有主线程上的任务执行完了,才通知”任务队列”,任务队列中的任务才会进入主线程执行。

执行栈

当执行某个函数、用户点击一次鼠标,Ajax完成,一个图片加载完成等事件发生时,只要指定过回调函数,这些事件发生时就会进入任务队列中,等待主线程读取,遵循先进先出原则。

执行任务队列中的某个任务,这个被执行的任务就称为执行栈。

主线程

要明确的一点是,主线程跟执行栈是不同概念,主线程规定现在执行执行栈中的哪个事件。

主线程循环:即主线程会不停的从执行栈中读取事件,会执行完所有栈中的同步代码。

当遇到一个异步事件后,并不会一直等待异步事件返回结果,而是会将这个事件挂在与执行栈不同的队列中,我们称之为任务队列(Task Queue)。

当主线程将执行栈中所有的代码执行完之后,主线程将会去查看任务队列是否有任务。如果有,那么主线程会依次执行那些任务队列中的回调函数。

JS 异步执行的运行机制

  1. 所有任务都在主线程上执行,形成一个执行栈。
  2. 主线程之外,还存在一个任务队列。只要异步任务有了运行结果,就在任务队列之中放置一个事件。
  3. 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列。那些对应的异步任务,进入执行栈开始执行。
  4. 主线程不断重复上面的第三步。

异步

  • 如果能直接拿到将结果

    那就是同步

    比如你在医院挂号,你拿到号才会离开窗口

    同步任务可能消耗 10 毫秒,也可能需要 3 秒

    总之不拿到结果你是不会离开的,这就是同步

  • 如果不能直接拿到结果

    那就异步

    比如你在餐厅门口等位,你拿到号后可以去干别的事比如逛街

    你可以每 10 分钟取餐厅问一下排到自己了没(轮询)

    也可以扫码用微信接受通知(回调)

异步举例

  • 以 AJAX 为例

    request.send() 之后,并不能直接得到 response

    console.log(request.response) 试试

    必须等到 readyState 变为 4 后,浏览器才会回头调用 request.onreadystatechange 函数

    我们才能得到 request.response

    这就跟餐厅给你发微信提醒的过程类似

  • 回调 callback

    你写给自己用的函数,不是回调

    你写给别人用的函数,就是回调

    request.onreadystatechange 就是写个浏览器调用的

    意思是你(浏览器)回头调用一下这个函数

  • 简单来理解就是:同步按你的代码顺序执行,异步不按照代码顺序执行,异步的执行效率更高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
getJSON.onclick = () => {
const request = new XMLHttpRequest();
request.open('GET', '/5.json');
request.onreadystatechange = () => {
if (request.readyState === 4 && request.status === 200) {
const object = JSON.parse(request.response);
myName.textContent = object.name;
console.log(request.response);
}
}
request.send();
// console.log(request.resopne)
setTimeout(() => {
console.log(request.response);
// 或者将 放在 request.onreadystatechange 函数中
}, 2000)
}
// request.send()执行完,再执行 request.onreadystatechange 函数
// 执行顺序为 response.open() => respone.send() => request.onreadystatechange

onreadystatechange 执行两次

回调

写了却不调用,给别人调用的函数,就是回调。需自行意会

函数例子:

1
2
3
4
5
function f1() {}
function f2(fn) {
fn()
}
f2(f1)

分析:

  1. 我调用 f1 没有?
  2. 我把 f1 传给 f2 (别人)了没有?
  3. f2 调用 f1 了没有?

答:1. 调用了。2.穿了。3.f2 调用了 f1。

那么,f1 是不是我写给 f2 调用的函数? 是。

所以,f1 是回调。


例子2:

1
2
3
4
5
6
7
8
function f1(x) {
console.log(x)
}
function f2(fn){
fn('hello')
}
f2(f1)
// hello

fn(‘hello’) 中的 fn 就是 f1 。

fn(’hello‘) 中的 ’hello’ 会被赋值给参数 x 。

所以 x 就是 ‘hello’。

异步和回调的关系

  • 关联

    异步任务需要再得到结果时通知 JS 来拿结果

    怎么通知?

    可以让 JS 留一个函数地址给浏览器(电话号码)

    异步任务完成时浏览器调用该函数即可(拨打电话)

    同时把任务作为参数传给该函数(通知)

    这个函数是我写给浏览器调用的,所以是回调函数

  • 区别

    异步任务需要用到回调函数来通知结果

    但回调函数不一定只用在异步任务里

    回调可以用到同步任务里

    array.forEach(n=>console.log(n)) 就是同步回调

判断同步异步

如果一个函数的返回值处于

  • setTimeout
  • AJAX(即 XMLHttpRequest)
  • AddEventListener

这三个东西内部中,那么这个函数就是异步函数

还有其他 API 是异步的,遇到再说。

举例说明:

摇骰子, 随机打印出1-6 中的一个数

1
2
3
4
5
6
function 摇骰子() {
setTimeout( () => { // 箭头函数
return parseInt(Math.random() * 6) + 1;
}, 1000)
// return undefined
}

分析:

摇骰子() 里没有写 return,那就是 return undefined

箭头函数里有 return,返回真正结果

所以这是一个异步函数/异步任务。

1
2
const n = 摇骰子();
console.log(n)

如何拿到异步结果?

用回调,写个函数,然后把函数地址给他

1
2
3
4
5
6
7
8
9
10
11
function f1(x) {
console.log(x);
}
// 然后要求 摇骰子函数 得到结果后 把结果作为参数 传给f1
function 摇骰子(fn){
setTimeout( () => {
fn(parseInt(Math.random() * 6) + 1)
}, 1000)
}

摇骰子(f1); // 结果

简化为箭头函数

1
2
3
4
5
6
7
8
9
10
11
function f1(x) { 
console.log(x);
}
摇骰子(f1);
// 改为
摇骰子(x => {
console.log(x);
});
// 再简化为
摇骰子(console.log);
// 如果参数个数不一致就不能这样简化,有个相关面试题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 面试题
const array = ['1', '2', '3'].map(parseInt);
console.log(array);
// 结果为 [1, NaN, NaN]
// parseInt('1', 0, arr) => 1
// parseInt('2', 1, arr) => NaN
// parseInt('3', 2, arr) => NaN

const array = ['1', '2' , '3'].map((item, i , arr) => {
return parseInt(item)
});
console.log(array)
// 正确结果 [1, 2, 3]

// 简写 永远使用箭头函数
const array = ['1', '2', '3'].map((item) => parseInt(item));

异步总结

  • 异步任务不能拿到结果
  • 于是我们传一个回调给异步任务
  • 异步任务完成时调用回调
  • 调用的时候把结果作为参数

异步任务两个结果,成功或失败

两个方法解决

方法一:回调接受两个参数

1
fs.readFile('./1.txt', (error, data) => {    if(error){        console.log('失败'); return;    }    console.log(data.toString()) // 成功})

方法二:两个回调

1
ajax('get', '/1.json', data => {/*成功回调*/}, error => {/*失败回调*/})ajax('get', '/1.json',{    success: () => {},    fail: () => {}})// 接受一个对象,对象有两个 key 表示成功和失败

这些方法的不足

不管方法一还是方法二,都有问题

  1. 不规范,名称五花八门,有人用 success + error,有人用 success + fail,done + fail
  2. 容易出现==回调地狱== ,代码变得看不懂
  3. 很难进行错无处理

回调地狱举例

1
getUser( user => {    getGroups(user, (groups) => {        groups.forEach( (g) => {            g.filter(x => x.ownerId === user.id)            .forEach(x => console.log(x))        })    })})// 仅示例,这只是四层,二十层呢,代码会很难读
Snipaste_2021-09-25_22-12-10
吐槽回调地狱 来源网络

如何解决回调问题,用 Promise

有什么办法能解决这三个问题:

  • 会犯回调的名字或顺序
  • 拒绝回调地狱,让代码可读性更强
  • 很方便地捕获错误

1976年,Daniel P.Friedman 和 David Wis 俩人提出 Promise 思想

后人基于此发明了 Future、Delay、Deferred等

前端结合 Promise 和 JS,制定了 Promise/A+规范

该规范详细描述了 Promise 的原理和使用方法。

以 AJAX 的封装为例,来解释 Promise

Promise MDN

1
// 示例ajax = (method, url, options) => {    const {success, fail} = option; // 析构赋值    // const succes = option.success;    // const fail = option.fail;    const request = new XMLHttpRequest();    request.open(method, url);    request.onreadystatechange = () => {        if (request.readyState === 4) {            // 成功就调用 success,失败 fail            if (request.status < 400) {                success.call(null, request.response);            }else if (request.status >= 400) {                fail.call(null, request, request.status);            }        }    }    request.rend();}ajax('get', '/xxx', {    success(response){}, // function 缩写    // success: function(response){}    fail: (request, status) => {}})

Promise 写法:

1
// 先改一下调用姿势ajax('get', '/xxx', {    success(response){},    fail: (request, status) => {}})// 上面用到了两个回调,还使用了 success 和 fail// 改成 Promise 写法ajax('get', '/xxx')    .then((response)=>{}, (request, status)=>{} )// 虽然也是回调// 但是不需要经济 success 和 fail 了// then 的第一个参数就是 success// then 的第二个参数就是 fail

请问 ajax() 返回了个啥 ?

返回了一个含有 .then() 方法的对象呗。

那么再请问如何得到这个含有 .then() 的对象呢 ?

那就要改造 ajax 的源码了。

return new Promise((resolve, reject) => {})

1
// 修改 ajaxajax = (method, url, option) => {    return new Promise((resolve, reject) => {        const {success, fail} = option;        const request = new XMLHttpRequest();        request.open(methon, url);        request.onreadystatechange = () => {            if(request.readyState === 4) {                if(request.status < 400) {                    // 成功                    resolve.call(null, request.response);                }else if(request.status >= 400) {                    // s                    reject.call(null, request);                }            }        }        request.send();    });}

小结

第一步:

  • retrun new Promise((resolve, reject) => {...})
  • 任务成功则调用 resolve(result)
  • 任务失败则调用 reject(error)
  • resolve 和 reject 会再去调用成功和失败函数

第二步:

  • 使用 .then(success, fail)传入成功函数和失败函数

我们自己封装的 ajax 的缺点:

post 无法上传数据。request.send(这里可以上传数据)

不能设置请求头。request.setRequestHeader(key, value)

使用:

Jquery.ajax

axios(推荐)