0%


title: HTTP基础

date: 2019-08-21 14:53:32

tags: HTML

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • HTML # 一级分类
  • HTTP基础 # 二级分类

ajax

ajax 是一种技术方案,核心依赖浏览器提供的 XMLHttpRequest 对象,是这个对象使得浏览器可以发出 http 请求与接受响应。

如何与后端交互

  1. form 表单提交.缺点会跳转,无后台反馈
  2. ajax
  3. websocket

实现方法

  1. XMLHttpRequest 对象(该对象支持同步和异步请求)
  2. fetch

范例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequst()
//true默认是异步的方式,false是同步的方式
xhr.open("get", "/xxx.com", true)
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
//成功
console.log(xhr.responseText)
} else {
console.log("服务器异常")
}
}
//发送请求
xhr.send()

Fetch

fetch 基于 promise

当接受到错误状态码时,conffetch()返回的 promise 不会标记为 reject

默认情况下,fetch 不会从服务端发送或接收任何 cookies.

API

1
2
3
4
5
6
7
8
9
10
11
12
13
1. let xhr = new XMLHttpRequest();
2. xhr.open('GET','/xxx') //初始化xhr
3. xhr.send() //发送请求
4. xhr.onreadystatechange //监听请求状态的变化<readystatechange>是一个事件
5. xhr.readyState === 1 //xhr.open()已经完成
6. xhr.readyState === 2 //xhr.send()已经完成
7. xhr.readyState === 3 //xhr.responseText正在下载
5. xhr.readyState === 4 //响应完成
6. xhr.status //HTTP状态码
7. var string = xhr.responseText //响应的内容
8. var value = JSON.parse(xhr.responseText) //把符合JSON语法的字符串转换成JS,解析响应返回的内容
9. value.node //若value是对象,这就是对象里内容
10. value.node.name //对象里的name的值

post形式需要将数据放入到 send 中

跨域

同源策略

同协议,域名,端口

实现跨域的方法

JSONP

JSON with padding

通过 script 标签加载数据的方式获取数据当做 js 代码来执行。jsonp 需要对应接口的后端配合才可以实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function addScriptTag(src) {
var script = document.createElement("script");
script.setAttribute("type", "text/javascript");
script.src = src;
document.body.appendChild(script);
}

window.onload = function () {
addScriptTag("http://example.com/ip?callback=foo");
};

function foo(data) {
console.log("Your public IP address is: " + data.ip);
}

上面代码通过动态添加<script>元素,向服务器 example.com 发出请求。注意,该请求的查询字符串有一个 callback 参数,用来指定回调函数的名字,这对于 JSONP 是必需的。

服务器收到这个请求以后,会将数据放在回调函数的参数位置返回。

1
2
3
foo({
ip: "8.8.8.8",
});

由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了 foo 函数,该函数就会立即调用。作为参数的 JSON 数据被视为 JavaScript 对象,而不是字符串,因此避免了使用JSON.parse的步骤。

必考面试题:JSONP 为什么不支持 POST 请求

答:因为 JSONP 是通过动态创建 script 的方法进行的,而 script 只能发送 get 请求不能发送 post 请求。

jquery 的写法

1
2
3
4
5
6
7
8
$.ajax({
url:"http://jack.com:8002/pay",
dataType: "jsonp",
success:function (response) {
console.log(response)
}
}
})

axios 写法

1
2
3
4
5
6
7
8
9
this.$http = axios;
this.$http
.jsonp("http://www.domain2.com:8080/login", {
params: {},
jsonp: "handleCallback",
})
.then((res) => {
console.log(res);
});

CORS

跨域资源共享,是一种 ajax 跨域请求资源的方式。

写法就是普通的 ajax 写法.不过在浏览器请求上加点处理.而且是浏览器自动处理的.

当使用 XMLHttpRequest 发送请求时,浏览器发现该请求不符合同源策略,会给请求加一个请求头:Origin

后台进行处理,如果确定接受请求,则在返回结果中加响应头:Access-Control-Allow-Origin

浏览器判断该响应头中是否包含Origin的值,如果有,浏览器会处理响应,就可以拿到响应数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var xhr = new XMLHttpRequest(); // IE8/9需用window.XDomainRequest兼容

// 前端设置是否带cookie
xhr.withCredentials = true;

xhr.open("post", "http://www.domain2.com:8080/login", true);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
xhr.send("user=admin");

xhr.onreadystatechange = function () {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};

postMessage

  1. A 页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
componentDidMount() {
window.addEventListener('message', this.receiveMessage, false)
const iframeImg = this.iframe.contentWindow
iframeImg.onload = function() {
iframeImg.postMessage('message', 'http://www.blogoog.com:8088')
}
}
receiveMessage = (event) =>{
const origin = event.origin || event.originalEvent.origin
const thisData = JSON.parse(event.data)
const imgUrl = Object.values(thisData)
this.setState({ imgUrl })
setTimeout(() =>{
this.closeIframe();
message.success('上传成功')
},1000)
if (origin !== 'http://www.blogoog.com:80881) {
window.location.hash = '#/exception/404'
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<iframe
className={styles.iframeWrap}
src="http://www.blogoog.com:8088"
name="imgIframe"
frameBorder="0"
scrolling="auto"
ref={(dom) => {
this.iframe = dom;
}}
>
您的浏览器不支持iframe
</iframe>
  1. A 页面使用的语法

window.postMessage()

otherWindow.postMessage(message, targetOrigin, [transfer])

  • otherWindow:其他窗口的一个引用(在这里我使用了 iframe 的 contentWindow 属性)
  • message:将要发送到其他 window 的数据(可以不受限制的将数据对象安全的传送给目标窗口而无需自己序列化,原因是因为采用了结构化克隆算法)
  • targetOrigin:接收信息的 URL(在这里我当然填的 B 页面的 URL)
  • transfer:可选参数

window.addEventListener(‘message’, receiveMessage, false)

target.addEventListener(type, listener, options)

  • type:表示监听事件类型的字符串
  • listener:当所监听的事件类型触发时,会通知的一个对象或者一个函数
  • potions:可选参数(在此我用 false,表示在 listener 被调用之后不会自动移除)

receiveMessage = (event) => {}

  • event.data:从另一个 window 传递过来的对象(包含传递过来的所有信息)
  • event.origin||event.originalEvent.origin:window.postMessage()发送消息的目标 URL
  • event.source:对发送消息的窗口对象的引用

注意点!!!

  • 在页面内嵌入 iframe 页面的情况下,需要等到页面内的 iframe 页面,也就是 B 页面加载完成之后,才能进行 postMessage 跨域通信
  • event.origin 中的 origin 不能保证是该窗口的当前 origin 或者未来 origin,因为 postMessage 被调用后,可能会被导航到不同的位置,所以需要做个异常情况判断处理origin !== 'http://www.blogoog.com:8088'
  1. B 页面
1
2
3
4
5
6
7
8
9
10
11
12
created() {
window.addEventListener{"message", this.receiveMessage, false)
},
receiveMessage(e) {
if (e origin === 'http://www.blogoog.com:8080' || e.origin === 'http://www.blogoog.com:8088') {
this.sendBtnShow = true
this.originSource = e.origin
this.magicBuildData = JSON.parse(e.data)
},
sendMsg() {
top.postMessage(JSON.stringify(data), 'http://www.blogoog.com:8000')
}
  1. B 页面使用到的语法

top.postMessage('data', 'http://www.blogoog.com:8000')

参考上面 A 页面的语法

为什么用 top 而不用 window 下面再讲

window.addEventListener('message', receiveMessage, false)

参考上面 A 页面的语法

receiveMessage = (event) => {}

参考上面 A 页面的语法

window.postMessage()中的 window 到底是啥?

始终是你需要通信的目标窗口

  • A 页面中:A 页面向 B 页面发送跨域信息,window 就是在 A 页面中嵌入的 iframe 指向的 B 页面的 window,即:iframe.contentWindow
  • B 页面中:B 页面想 A 页面发送跨域信息,window 就是 A 页面的 window,在这里因为 B 页面时嵌入到 A 页面中的,对于 B 页面来讲,window 就是 top 或者 parent

需要特别注意的坑

  1. 一定要等 A 页面嵌入的 B 页面加载完成之后,再进行 postMessage 跨域通信
  2. 一定要对 origin 做判断,去掉不是来自我们目标窗口的 origin,防止来自其他 origin 的攻击
  3. 着重注意 window.postMessage()中 window 的用法,明确目标窗口的 window

websocket 协议跨域

WebSocket protocol 是 HTML5 一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是 server push 技术的一种很好的实现。

原生 WebSocket API 使用起来不太方便,我们使用 Socket.io,它很好地封装了 webSocket 接口,提供了更简单、灵活的接口,也对不支持 webSocket 的浏览器提供了向下兼容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
// 监听服务端消息
socket.on('message', function(msg) {
console.log('data from server: ---> ' + msg);
});

// 监听服务端关闭
socket.on('disconnect', function() {
console.log('Server socket has closed.');
});
});

document.getElementsByTagName('input')[0].onblur = function() {
socket.send(this.value);
};
</script>

后端 node

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 http = require("http");
var socket = require("socket.io");

// 启http服务
var server = http.createServer(function (req, res) {
res.writeHead(200, {
"Content-type": "text/html",
});
res.end();
});

server.listen("8080");
console.log("Server is running at port 8080...");

// 监听socket连接
socket.listen(server).on("connection", function (client) {
// 接收信息
client.on("message", function (msg) {
client.send("hello:" + msg);
console.log("data from client: ---> " + msg);
});

// 断开处理
client.on("disconnect", function () {
console.log("Client socket has closed.");
});
});

标签

iframe

HTML 内联框架元素<iframe> 表示嵌套的 browsing context。它能够将另一个 HTML 页面嵌入到当前页面中。

1
2
3
4
5
6
7
8
<iframe
src="https://mdn-samples.mozilla.org/snippets/html/iframe-simple-contents.html"
title="iframe example 1"
width="400"
height="300"
>
<p>Your browser does not support iframes.</p>
</iframe>

canvas

标签定义图形,比如图表和其他图像。该标签基于 JavaScript 的绘图 API

source

定义多媒体资源 <video><audio>

command

定义命令按钮,比如单选按钮、复选框或按钮

figure

规定独立的流内容(图像、图表、照片、代码等等)

导航

section

定义文档中的节(section、区段)

HTTP 报文

HTTP 报文是在 HTTP 应用程序之间发送的数据块。这些数据块以一些文本形式的元信息开头,描述报文的内容及含义,后面跟着可选的数据部分

URI 和 URL

URI:统一资源标识符

URL:统一资源定位符

报文组成

  1. 对报文进行描述的起始行 —— start line
  2. 包含属性的首部块 —— header
  3. 可选的包含数据的主体部分 —— body

报文语法

  1. 请求报文:

向 web 服务器请求一个动作

1
2
3
4
<method><request-URL><version>
<headers>

<entity-body>
  1. 响应报文

把请求结果返回给客户端

1
2
3
4
<version><status><reason-phrase>
<headers>

<entity-body>

状态码

服务器和浏览器之间的约定

分类:

100-199 用于指定客户端应相应的某些动作。

200-299 用于表示请求成功。

300-399 用于已经移动的文件并且常被包含在定位头信息中指定新的地址信息。

400-499 用于指出客户端的错误。

500-599 用于支持服务器错误。

状态码 状态信息 解释
100 Continue 初始请求已接受,继续发送
101 switching Protocols 服务器遵从客户请求转换到另一种协议
200 OK 请求成功
201 Created 服务器已经创建了文档,Location 头给出了他的 URL
202 Accepted 已接受请求,但处理尚未完成
203 Non-Authoritative Information 文档已正常返回但一些应答头可能不正确,因为使用的是文档的拷贝
204 No Content 服务器接到请求已经处理完毕,但浏览器页面不会刷新
205 Reset Content 没有新内容,但浏览器应该重置它所显示的内容,用来强制浏览器清除表单输入内容.
206 Partial Content 客户发送了一个带有 Range 头的 GET 请求,服务器完成了它
300 Multiple Choices 客户请求的文档可以在多个位置找到,这些位置已经在返回的文档中列出.如果服务器要提出优先选择,则应该在 Location 应答头指明
301 Moved Permanently 永久重定向,请求的网页已经永久移动到新位置
302 Found 临时重定向,POST 方法的重定向在未询问用户的情况下就会变成 GET
303 See Other 类似 301/302,不同之处在于,原来的请求是 POST,现在是 GET
304 Not Modified 自从上次请求后,网页未被修改过
305 Use Proxy 使用代理服务器
307 Temporary Redirect 临时重定向,不同之处,把 POST 转为 GET
400 Bad Request 请求出现语法错误
401 Unauthorized 用户未授权,需要用户验证
403 Forbidden 服务器已经理解请求,但是拒绝执行
404 Not Found 服务器找不到请求的网页
405 Method Not Allowed 请求方法(GET、POST、HEAD、DELETE、PUT、TRACE 等)对指定的资源不适用。
406 Not Acceptable 指定的资源已经找到,但它的 MIME 类型和客户在 Accpet 头中所指定的不兼容
407 Proxy Authentication Required 类似于 401,表示客户必须先经过代理服务器的授权
408 Request Timeout 请求超时
500 Internal Server Error 服务器遇到错误,无法完成请求
503 Service Unavailable 由于临时的服务器维护或过载,暂时无法处理请求
504 Gateway Timeout 由作为代理或网关的服务器使用,表示不能及时地从远程服务器获得应答
505 HTTP Version Not Supported 服务器不支持请求中所指明的 HTTP 版本。

HTTP 请求方法

  1. GET

请求服务器发送某个资源

  1. HEAD

与 GET 方法类似,但是在服务器的响应中没有资源的内容,只有资源的一些基本信息,主要用于

  • 在不获取资源的情况下获取信息(类型,大小等)
  • 通过状态码看资源是否存在
  • 通过查看首部,测试资源是否被修改
  1. PUT

和 GET 从服务器获取资源相反,PUT 用于向服务器写入资源.PUT 的语义就是让服务器用请求的主体部分创建一个请求 URL 命名的文档.如果存在就替换

  1. POST

POST 用于向服务器发送数据,通常用来支持 HTML 的表单(input,select,textarea),表单中的数据会被发送到服务器

  1. TRACE

客户端发送一个请求,这个请求可能会穿过防火墙,代理,网关,和一些其他应用程序,每个中间节点都可以修改 HTTP 请求,TRACE 方法允许客户端在最终请求发往服务器的时候,看看他变成了什么样子

TRACE 请求会在目的服务器端发送一个”闭环”诊断,行程最后一站服务器会弹回一条 TRACE 响应,并在响应主题中携带他收到的原始请求报文

  1. DELETE

用于要求服务器删除请求的 URL,和 PUT 一样,服务器可能不支持

  1. OPTIONS

用于请求服务器告知其支持的各种功能

浏览器缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术.

HTTP 缓存有多种规则,根据是否需要重新向服务器发起请求来分类,分为两大类(强制缓存,协商缓存)

http 缓存机制主要在 http 响应头中设定,响应头中相关字段为Expires、Cache-Control、Last-Modified、Etag

cache-control

cache-control 通常用于优化网页性能,适当使用可减少重复读取相同内容

可以在服务端 setHeader 设置 cache-control

可以设置让该字段存于缓存中,浏览器下次访问时不发起请求,直接从缓存中读取该资源

ETag

ETag 通常用于优化网页性能,适当使用可减少重复读取相同内容

可通过服务端 setHeader 设置 etag 为相应的字段的 md5 值

浏览器访问时,request.headers 会带有 if-none-match 属性(为上述 MD5),若不等于则说明服务器上的

该资源与本地存储的该资源不相同,需要重新从服务器加载;若相等则说明两者资源相同,则直接从本地

缓存获取.后端应设置 statusCode=304.与 cache-control 不同的是:etag 始终都要发请求和收响应,而

cache-control 不发请求.

cookie

存储信息到浏览器,可以用 js 设置,也可以在服务器端通过 set-cookie 让浏览器种下 cookie,存在于响应头里。

每次请求都会带上 cookie,最大容量 4k

用途:记录用户名

设置 cookie 时的参数:

  1. path: cookie 影响到的路径,匹配该路径才发送 cookie
  2. expiresmaxAge:告诉浏览器 cookie 多久过期

不设置就会产生 session cookie,当关闭浏览器,cookie 就被清除 3. secure: 为 true 时,在 http 无效,在 https 生效 4. httpOnly: 浏览器不允许通过 js 修改 cookie

session

用来保存状态。当用户输入用户名密码提交给服务器后,服务器验证通过后创建 session 用于记录用户信息,session 可保存在服务器,也可以保存在数据库

  1. 创建 session 后,会把关联的 session_id 通过是 set-cookie 添加到响应头
  2. 浏览器加载页面发现有 set-cookie 字段,就把这个 cookie 种到指定域名下
  3. 下次刷新页面,发送的请求带上 cookie,服务器接受后根据 session_id 来识别用户

cookie 和 session 的区别:

cookie 是存储在浏览器中的一小段数据,session 是让服务器识别某个用户的机制 session.在实现过程中需要使用 cookie.二者不是同一维度的东西.

localStorage

  1. 本地存储,永不过期,除非手动 js 删除。5M 大小。
  2. 不参与网络传输
  3. 一般用于性能优化

localStorage 的存储,获取,删除

只支持字符串存储.

1
2
3
4
5
6
7
8
9
10
11
//存储
localStorage.setItem("myCat", "Tom");

//读取
localStorage.getItem("Tom");

//移除
localstorage.removeItem("myCat");

//移除所有
localstorage.clear();

sessionstorage

与 localstorage 类似,属于本地存储与上面的 session 无关.但是关闭浏览器失效.

浏览器缓存机制通俗语言版(自我理解)

1
2
Cache-Control: max-age=300;
ETag:W/"e-cbxLFQW5zapn79tQwb/g6Q"

浏览器第一次请求 a.jpg 这张图片,服务器发回一张图片,并且附送一个ETag(相当于一个用于验证的文件).浏览器把这张图片和 ETag 缓存到本地.如果在 300 秒内,浏览器又请求这张图片,那么直接从缓存中读取这张图.如果超过 300 秒,浏览器发现已经过了时间了,就重新向服务器发请求,并且附带之前给的 ETag.服务器拿着当前文件计算后的ETag和浏览器发回的ETag比较,如果一样,说明图片没换,发回一个响应头(不包含图片,304,表示文件没修改,还能用).如果不一样,就把新的文件和新的ETag发回去.

另外还有Last-Modified/If-Modified-Since.和ETag功能类似.因为服务器可能不止一个,文件存储在不同的服务器,那就给请求文件上附带上Last-Modified声明,请求时带上If-Modified-Since,表明服务器上一次修改这个文件的时间,拿来和服务器上记录的最后修改时间对比.如果修改时间比较新,说明被动过,发回一个新文件.如果时间比较旧,说明不用换,发回 304,使用缓存即可.

严谨说法

浏览器缓存控制分为强缓存协商缓存协商缓存必须配合强缓存使用。

首先浏览器第一次跟服务器请求一个资源,服务器在返回这个资源和 response header 的同时,会根据开发者要求或者浏览器默认,在 response 的 header 加上相关字段的 http response header。

一、当浏览器对某个资源的请求命中了强缓存时,利用[Expires]或者[Cache-Control]这两个 http response header 实现

  1. [Expires]:描述的是一个绝对时间,根据的是客户端时间。用 GMT 格式的字符串表示,如:Expires:Thu, 31 Dec 2037 23:55:55 GMT 下次浏览器再次请求同一资源时。先从客户端缓存中寻找,找到这个资源后,拿出它的[Expires]跟当前的请求时间比较。如果请求时间在[Expires]指定的失效时间之前,就能命中缓存,这样就不用再次到服务器上去缓存一遍,节省了资源。但是正因为是绝对时间,如果客户端时间被随意更改下,这个机制就失效了。所以我们需要[Cache-Control]。

  2. [Cache-Control]:描述的是一个相对时间,在进行缓存命中时,都是利用浏览器时间判断。

这两个 header 可以只启用一个,也可以同时启用,当 response header 中,[Expires]和[Cache-Control]同时存在时,[Cache-Control]优先级高于[Expires]。

二、当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中。

如果命中,则还是从客户端缓存中加载。协商缓存利用的是[Last-Modified,If-Modified-Since]和[ETag、If-None-Match]这两对 Header 来管理的。

  1. [Last-Modified]:原理和上面的[expires]相同,区别是它是根据服务器时间返回的 header 来判断缓存是否存在。但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况(这种问题也不容易被定位),这时候我们需要[ETag、If-None-Match]。

  2. [ETag、If-None-Match]:原理与上相同,区别是浏览器跟服务器请求一个资源,服务器在返回这个资源的同时,在 respone 的 header 加上 ETag 的 header,这个 header 是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同。

  3. [ETag、If-None-Match]这么厉害我们为什么还需要[Last-Modified、If-Modified-Since]呢?有一个例子就是分布式系统尽量关闭掉 ETag(每台机器生成的 ETag 都会不一样)

[Last-Modified,If-Modified-Since]和[ETag、If-None-Match]一般都是同时启用。

HTTPS 原理

HTTP+SSL/TLS, 即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL,用于安全的 HTTP 数据传输。

加密算法:

  1. 对称加密

有流式、分组两种,加密和解密都是使用的同一个密钥。 2. 非对称加密

加密使用的密钥和解密使用的密钥是不相同的,分别称为:公钥、私钥,公钥和算法都是公开的,私钥是保密的。非对称加密算法性能较低,但是安全性超强,由于其加密特性,非对称加密算法能加密的数据长度也是有限的 3. 哈希算法

将任意长度的信息转换为较短的固定长度的值,通常其长度要比信息小得多,且算法不可逆。 4. 数字签名

签名就是在信息的后面再加上一段内容(信息经过 hash 后的值),可以证明信息没有被修改过。hash 值一般都会加密后(也就是签名)再和信息一起发送,以保证这个 hash 值不被修改。

简单通信流程

概括来说,整个简化的加密通信的流程就是:

  1. 小明访问网站,网站将自己的证书给到小明(其实是给到浏览器,小明不会有感知)
  2. 浏览器从证书中拿到网站的公钥 A
  3. 浏览器生成一个只有自己知道的对称密钥 B,用公钥 A 加密,并传给网站(其实是有协商的过程,这里为了便于理解先简化)
  4. 网站通过私钥解密,拿到对称密钥 B
  5. 浏览器、网站 之后的数据通信,都用密钥 B 进行加密

存在的问题

证书可能存在的问题

证书是伪造的:压根不是 CA 颁发的

证书被篡改过:比如将 XX 网站的公钥给替换了

解决方法: 数字签名和摘要

“摘要”就是对传输的内容,通过 hash 算法计算出一段固定长度的串。然后,在通过 CA 的私钥对这段摘要进行加密,加密后得到的结果就是“数字签名.这段数字签名只有 CA 的公钥才能够解密。

一个页面从输入 URL 到页面加载显示完成,这个过程中都发生了什么?(流程说的越详细越好)

详细版:

1、浏览器会开启一个线程来处理这个请求,对 URL 分析判断如果是 http 协议就按照 Web 方式来处理;

2、调用浏览器内核中的对应方法,比如 WebView 中的 loadUrl 方法;

3、通过 DNS 解析获取网址的 IP 地址,设置 UA 等信息发出第二个 GET 请求;

4、进行 HTTP 协议会话,客户端发送报头(请求报头);

5、进入到 web 服务器上的 Web Server,如 Apache、Tomcat、Node.JS 等服务器;

6、进入部署好的后端应用,如 PHP、Java、JavaScript、Python 等,找到对应的请求处理;

7、处理结束回馈报头,此处如果浏览器访问过,缓存上有对应资源,会与服务器最后修改时间对比,一致则返回 304;

8、浏览器开始下载 html 文档(响应报头,状态码 200),同时使用缓存;

9、文档树建立,根据标记请求所需指定 MIME 类型的文件(比如 css、js),同时设置了 cookie;

10、页面开始渲染 DOM,JS 根据 DOM API 操作 DOM,执行事件绑定等,页面显示完成。

简洁版:

浏览器根据请求的 URL 交给 DNS 域名解析,找到真实 IP,向服务器发起请求;

服务器交给后台处理完成后返回数据,浏览器接收文件(HTML、JS、CSS、图象等);

浏览器对加载到的资源(HTML、JS、CSS 等)进行语法解析,建立相应的内部数据结构(如 HTML 的 DOM);

载入解析到的资源文件,渲染页面,完成。

WebSocket 协议

1.为什么需要 WebSocket

HTTP 协议只能由客户端发起,WebSocket 可以实现双向对话,服务端和客户端都可以发送信息.

2.WebSocket 特点

  1. 建立在 TCP 协议之上,服务端实现容易.
  2. 与 HTTP 协议有着良好的兼容性,并且握手阶段采用 HTTP 协议,因此握手阶段不容易屏蔽,可通过 HTTP 代理服务器.
  3. 数据格式轻量,性能开销小,通信高效.
  4. 可以发送文本和二进制数据.
  5. 没有同源限制.
  6. 协议标识符是 ws(加密是 wss),服务器网址是 URL.
1
ws://example.com:80/some/path

客户端示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function (evt) {
console.log("Connection open ...");
ws.send("Hello WebSockets!");
};

ws.onmessage = function (evt) {
console.log("Received Message: " + evt.data);
ws.close();
};

ws.onclose = function (evt) {
console.log("Connection closed.");
};

WebSocket 客户端 API

WebSocket 构造函数

1
ws = new WebSocket("ws://localhost:8080");

执行上面语句之后,客户端就会与服务器进行连接。

WebSocket.readyState

readyState 属性返回实例对象的当前状态,共用四种.

  • CONNECTING: 值为 0,表示正在连接.
  • OPEN: 值为 1,表示连接成功,可以通信.
  • CLOSEING: 值为 2,表示正在关闭.
  • CLOSED: 值为 3,表示连接已经关闭,或者打开失败.

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch (ws.readyState) {
case WebSocket.CONNECTING:
// do something
break;
case WebSocket.OPEN:
// do something
break;
case WebSocket.CLOSING:
// do something
break;
case WebSocket.CLOSED:
// do something
break;
default:
// this never happens
break;
}

WebSocket.onopen

实例对象的onopen属性,用于指定连接成功后的回调函数.

1
2
3
ws.onopen = function () {
ws.send("hello");
};

如果指定多个函数,可以使用addEventListener方法.

1
2
3
ws.addEventListener("open", function (event) {
ws.send("hello");
});

WebSocket.onclose

实例对象的onclose属性,用于指定连接关闭后的回调函数。


title: React-Hooksdate: 2020-06-27 09:13:34

tags: React

class 组件类的缺点

  • 大型业务很难拆分或重构,也很难测试
  • 业务逻辑分散在组件的各个方法中,导致重复逻辑或关联逻辑.
  • 组件类引入了复杂的编程模式,比如render props和高阶组件.

React Hook 的设计目的

加强版函数组件,完全不使用类,就能写出一个全功能组件.

含义为,组件尽量写成纯函数,如果需要外部功能或副作用,就用钩子函数把外部代码”钩”进来.

React 规定,钩子一律使用use前缀命名,便于识别.

常用钩子: useState(),useContext(),useReducer(),useEffect()

useState()状态钩子

用于为函数组件引入状态(state),纯函数不能有状态,所以将状态放入钩子里.

1
2
3
4
5
6
7
8
9
import React, { useState } from 'react';
export default function Button(){
const [buttonText, setButtonText] = useState("Click!")

function handleCilck(){
return setButtonText("thank")
}
return <button onClick={handleClick}>{buttonText}</button>>
}

用户点击后,文字自发生了变化,文字取决于用户是否点击,这就是状态.

Button 组件是一个函数,内部使用useState()引入状态.

useState()函数接收状态的初始值,作为参数,例子中的初始值为按钮的文字,该函数返回一个数组,数组的第一个成员是一个变量(buttonText).

指向状态的当前值.第二个成员是一个函数,用来更新状态.约定是set前缀加上变量名(setButtonText).

useContext()共享状态钩子

如果需要在组件之间共享状态,可以使用useContext().

1
2
3
4
5
6
7
8
const AppContext = React.createContext({})

<AppContext.Provider value={{ username: "super"}}>
<div className="App">
<Navbar />
<Message />
</div>
</AppContext.Provider>

其中,AppContext.Provider提供一个Context对象,这个对象可以被子组件共享.

1
2
3
4
5
6
7
8
9
10
//Navbar.js
const Navbar = () => {
const { username } = useContext(AppContext);
return (
<div className="navbar">
<p>Awesome</p>
<p>{username}</p>
</div>
);
};

其中,useContext()钩子函数用来引入 Context 对象,从中获取username属性.

1
2
3
4
5
6
7
8
9
10
//Message.js
const Message = () => {
const { username } = useContext(AppContext);
return (
<div className="messages">
<p>message for {username}</p>
<p className="message">useContext</p>
</div>
);
};

useReducer(): action 钩子

React 本身不提供状态管理功能,通常使用外部库,如 Redux.

Redux 的核心概念,组件发出 action 与状态管理器通信.状态管理器收到 action 后,使用 Reducer 函数算出新的状态,

Reducer 函数的形式是(state, action)=>newState.

useReducers()用来引入 Reducer 功能.

1
const [state, dispatch] = useReducer(reducer, initialState);

上面是useReducer()的基本用法,它接收 Reducer 函数和状态的初始值作为参数,返回一个数组.数组的第一个成员是状态的当前值,

第二个是发送 action 的dispatch函数.

由于 Hooks 可以提供共享状态和 Reducer 函数,所以这些方面可以取代 redux.但是无提供中间件和时间旅行.

useEffect(): 副作用钩子

useEffect()用来引入具有副作用的操作,如向服务器发送请求数据.

之前放在componentDidMount之中的代码可以放在useEffect()中.

解决了没有生命周期的问题.

用法如下:

1
2
3
useEffect(() => {
//async action
}, [dependencies]);

useEffect()接收两个参数,第一个参数是一个函数,异步操作的代码放在里面.

第二个参数是一个数组,用于给Effect的依赖项,只要这个数组发生变化,useEffect()就会执行.

第二个参数可以省略,这样每次渲染,就会执行useEffect().

第二个参数如果是一个空数组[],就只会在加载时执行一次,数据更新时不执行.

自定义 Hooks


title: React 路由 date: 2020-05-10 13:59:49

tags: React

安装 react-router-dom

可以使用<Link>,<NavLink>标签

1
npm install react-router-dom --save

创建路由模式

  1. hash 模式
  2. history 模式
1
2
3
4
//hash模式
import { BrowserRouter as Router, Link, Route } from "react-router-dom";
//history模式
import { Link, Route, HashRouter as Router } from "react-router-dom";

as类似于起别名,方便简化名称.

创建路由

需要通过使Route进行对组件的声明配置,才能被Link找到.

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
40
41
42
43
44
45
46
47
48
import React from "react";
// histoty
import { BrowserRouter as Router, Link, Route, Switch } from "react-router-dom";
// hash
// import { Link, Route, HashRouter as Router } from 'react-router-dom';

function Page1() {
return <h1>我是Page1</h1>;
}

function Page2() {
return <h1>我是Page2</h1>;
}

function Page3() {
return <h1>我是Page3</h1>;
}

class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}

render() {
return (
<div className="App">
<Router>
<ul>
<li>
<Link to="page1">Page1</Link>
</li>
<li>
<Link to="page2">Page2</Link>
</li>
<li>
<Link to="page3">Page3</Link>
</li>
</ul>
<Route exact path="/page1" component={Page1}></Route>
<Route exact path="/page2" component={Page2}></Route>
<Route exact path="/page3" component={Page3}></Route>
</Router>
</div>
);
}
}
export default App;

函数式(带组件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//router.js
import React from "react";
import { BrowserRouter as Router, Route, Switch, Link } from "react-router-dom";
import Login from "./pages/login";
import Home from "./pages/home";
import App from "./pages/app";

//函数式
export default function ARouter() {
return (
<Router>
<Switch>
<Route exact path="/" component={App}></Route>
<Route path="/home" component={Home}></Route>
<Route path="/login" component={Login}></Route>
</Switch>
</Router>
);
}
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
import React from "react";

import { Link } from "react-router-dom";

function App() {
return (
<div className="App">
<Link to="/home">去首页</Link>
<Link to="/login">去登陆</Link>
</div>
);
}

//有状态组件点击跳转
class App extends React.Component {
handleJump = () => {
this.props.histroy.push("/login");
};
render() {
return (
<div className="container">
<Link to="/login"></Link>
<Link to="/home"></Link>
<Button onClick={handleJump}>点击去登陆</Button>
</div>
);
}
}

export default App;

如果用Switch,浏览器输入/home也只会加载 App 页,因为/home先匹配了/

加上 exact 可以精准匹配

如果导入的是HashRouter,会自动加上#http://localhost:3000/login#/

手动跳转

当你使用 History 路由的时候,某些时候需要主动的跳转道某个路由,

这时又不能去触发节点行为,所以这个时候就可以通过 API 的方式,进行跳转.

1
2
3
4
5
6
// 跳转页面
this.props.history.push(参数和to的内容相似);
this.props.history.push("page1");

// 重定向页面
this.props.history.replace("page2");

当然还有 hash 的 go 方法。

动态路由

路由传值

路由传参(传值)一般使用paramsquery.

通过给to传递一个对象的方式进行数据传递.

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
<div className="App">
<Router>
<ul>
<li>
<Link
to={{
pathname: "/page1/10",
search: "?roles=[10, 20]",
state: { name: "Tom" },
}}
>
Page1
</Link>
</li>
<li>
<Link to="page2">Page2</Link>
</li>
<li>
<Link to="page3">Page3</Link>
</li>
</ul>
<Route exact path="/page1/:id" component={Page1}></Route>
<Route exact path="/page2" component={Page2}></Route>
<Route exact path="/page3" component={Page3}></Route>
</Router>
</div>

可以看到,向page1的路由上添加了一个:id表示需要传递paramid的值,

同时声明了search的文本和state对象多种方式传递了参数。以便根据不同的场景使用。

路由 Hooks

useParams: 获取参数(内部封装)

如果在有状态组件(也就是 class 声明的类中)获取参数,需要通过this.props.match.parmas.id.

而在函数式组件(无状态组件)中获取,需要通过useParams()

useHistory: 通过 JS Api 操作路由跳转

函数式组件没有作用域,this 为 undefined.所以需要使用 useHistory 进行跳转.

如果在有状态组件中,需要this.props.history.push('/')进行跳转.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function Detail() {
const params = useParams();
const history = useHistory();
return (
<div>
<p>当前的参数值为: {params.goodId}</p>
<button
onClick={() => {
history.push("/");
}}
>
跳转
</button>
</div>
);
}


title: React 入门 date: 2020-05-05 09:48:17

tags: React

React 是一个 MVC 框架.

React 特点

  1. 声明式写法
  2. 组件化
  3. 可开发多种任务

开发环境

官方脚手架 creat-react-app

需求: node > 6.0

安装命令npm install create-react-app

创建命令create-reate-app my-project

运行命令npm start

文件入口

/src/App.js

基本语法

ReactDOM.render()

React.CreateElement()

React.Component

1
2
3
4
5
6
7
8
//括号内分别为标签名,属性,默认值
const hello = React.CreatElement(
"h1",
{ className: "red", name: "Tom" },
"hello world"
);
//括号内为所要渲染元素,挂载位置
ReactDOM.render(hello, document.getELementById("app"));

特点:

当插入很多数据,ReactDOM.render 会通过虚拟 DOM 方式生成一个 diff,只插入一次.

1
2
3
4
5
6
7
8
//使用大括号形式将js函数编入HTML
const name = "Tom";
const ele = (
<h1 className="red" name="Tom">
hello,{name}
</h1>
);
ReactDOM.render(ele, document.getELementById("app"));

书写格式

src 下新建一个组件 Demo.js

1
2
3
4
5
6
7
8
9
10
11
12
//src/Demo.js
//引入react
import React from 'react'
//新建类class
class Demo extends React.Component {
//render方法渲染
render() {
return <h1> Hello React <h1>
}
}

export default Demo

组件挂载

将组件挂载到 ReactDOM 节点上

1
2
3
4
5
6
//index.js
import React from "react";
import ReactDOM from "react-dom";
import Demo from "./Demo";

ReactDom.render(<Demo />, document.getElementById("root"));

JSX

javascript 语法扩展.

在花括号{}中可以添加任意 js 的表达式(expressions).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//Demo.js
import React from "react";

class Demo extends React.Component {
render() {
const todoList = ["Learn React", "Learn Redux"];
return (
<div>
<h1>Hello React</h1>
<ul>
{todoList.map((item) => (
<li>{item}</li>
))}
</ul>
</div>
);
}
}

export default Demo;

JSX 实质

JSX 是语法糖–React.createElement()

JSX 返回 ReactElement 对象

实操总结

//jsx 中函数必须用{}括起来,不要有空格,否则失效

//jsx 中调用类中的函数要加 this

//在 jsx 之前要把要用的参数提前解构出来

//函数中调用也要解构

组件通信(属性传值)Props/State/Forms

组件和 Props(属性)

  • 组件像函数一样,接受特定的输入(props),产生特定的输出(React Elements)
  • V = f(props)
  • props 像纯函数一样,只读,不可变化

props.children可以拿到 div 下的首尾标签的子元素

示例:

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
import React from "react";

class NameCard extends React.Component {
//使用render()进行渲染
render() {
const { name, number, isHuman, tags } = this.props;
return (
<div>
<h4>{name}</h4>
<ul>
<li>电话: {number}</li>
<li>{isHuman ? "人类" : "外星人"}</li>
<hr />
<p>
{tags.map((tag, index) => (
<span key={index}>{tag}</span>
))}
</p>
</ul>
</div>
);
}
}
export default NameCard;

函数式组件(无状态组件)

无状态,没有生命周期

示例一

1
2
3
4
5
6
7
8
9
10
function Hello(props) {
return (
<div>
<h1>hello,{props.name}</h1>
<p>年龄: {props.age}</p>
</div>
);
}

ReactDOM.render(<Hello name="Tom" age="12" />, document.getElementById("app"));

示例二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const NameCard = (props) => {
render(){
const {name, number, isHuman, tags } = this.props
return (
<div>
<h4>{name}</h4>
<ul>
<li>电话: {number}</li>
<li>{ isHuman ? '人类' : '外星人' }</li>
<hr/>
<p>
{ tags.map((tag,index) => (
<span key={index}>{tag}</span>
))}
</p>
</ul>
</div>
)
}
}

State(状态)

  • 组件内部的数据可以动态改变
  • this.setState()是更新 state 的唯一途径

示例:LikesButton

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
//cpmponents/LikesButton.js
import React from "react";

class LikesButton extends React.Component {
constructor(props) {
super(props);
this.state = {
likes: 0,
};
//如果未使用箭头函数说明this,需绑定this
this.increaseLikes = this.increaseLikes.bind(this);
}

render() {
return (
<div>
<button
type="button"
// 函数要使用驼峰式,最好使用箭头函数,否则this指向不明
onClick={() => {
this.increaseLikes();
}}
>
点赞 {this.state.likes}
</button>
</div>
);
}
}

export default LikesButton;

生命周期

  • 组件挂载
  • 组件更新
  • 组件卸载
  • 错误处理

挂载

当组件实例被创建并插入 DOM 中,生命周期调用顺序为

  • constructor()

构造函数初始化,最先被执行,初始化state

  • static getDerivedStateFromProps()

这是一个静态方法,需要在前面增加 static 的属性

  • render()

渲染函数,返回渲染的内容,当页面产生更新也会触发该方法。

render()方法是class组件中唯一必须实现的方法。

  • componentDidMount

组件挂载之后,这个时候组件已经挂载完毕了

更新

当组件的propsstate发生变化时会触发更新。组件更新的生命周期调用顺序如下:

  • static getDrivedStateFromProps()

组件即将被更新,这里参数分别对应前后被修改的内容,通过返回一个布尔值告知是否需要更新视图。

  • shouldComponentUpdate()

如果shouldComponentUpdate()返回false,则不会调用render()

  • render()

当视图更新,那么 Render 也会重新更新

  • getSnapshotBeforeUpdate

getSnapshotBeforeUpdaterender之后componentDidUpdate之前输出,类似于中间件用来做一些捕获操作。

  • componentDidUpdate

componentDidUpdate(prevProps, prevState, snapshot),三个参数 prevProps,prevState,snapshot,表示之前的 props,之前的 state,和 snapshot。snapshot 是 getSnapshotBeforeUpdate 返回的值.

卸载

当组件从 DOM 中移除时会调用如下方法:

componentWillUnmount

组件卸载,我们可以清除一些定时器,取消网络请求。

错误处理

当渲染过程,生命周期,或子组件的构造函数中抛出错误时,会调用如下方法:

  • static getDrivedStateFromError()
  • componentDidCatch()
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
40
41
42
43
44
45
46
47
48
49
constructor (props) {
super(props)
this.state = {}
console.log('1.constructor构造函数')
}

componentDidMount () {
console.log('componentDidMount')
Store.subscribe(() => {
this.setState({})
})
}

static getDerivedStateFromProps (nextProps, prevState) {
console.log('getDerivedStateFromProps')
console.log(nextProps, prevState)
return true
}

getSnapshotBeforeUpdate (prevProps, prevState) {
console.log(prevProps, prevState)
return 'top: 200'
}

componentDidUpdate (prevProps, prevState, snapshot) {
console.log(prevProps, prevState, snapshot)
}

componentWillUnmount () {
console.log('componentWillUnmount')
}

changeText () {
Store.dispatch({
type: 'changeName',
value: '我是ClassDemo中修改的名字: wangly'
})
}

render () {
console.log('3.render函数')
return (
<div className="App">
<p>{ Store.getState().redux_name }</p>
{ this.state.redux_name }
<button onClick={ this.changeText.bind(this) }>更改文本</button>
</div>
)
}

示例: 电子钟表

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
40
41
42
43
44
45
46
47
48
49
50
51
52
//components/DigitalClock.js
import React from "react";

class DigitalClock extends React.Component {
constructor(props) {
//初始化props
super(props);
//初始化状态
this.state = {
//初始值
date: new Date(),
};
}
//加载前可以发起ajax请求,拉取接口数据
componentWillMount() {
console.log("组件加载前");
}
//挂载后使用定时器更新时间
componentDidMount() {
this.timer = setIntervalo(() => {
this.setState({
date: new Date(),
});
}, 1000);
}

//结束时,卸载
componentWillUnmount() {
clearInterval(this.timer);
}
//组件是否应该更新
shouldCompnentUpdate() {
return true;
}
//为true时执行数据更新
componentWillUpdate() {
console.log("数据将要更新");
}
componentDidUpdate() {
console.log("数据已经更新");
}

render() {
return (
<div>
<h1>{this.state.date.toLocalTimeString()}</h1>
</div>
);
}
}

export default DigitalClock;

事件处理中 this 的绑定

  1. 箭头函数
  2. class 中 bind 绑定
  3. 事件中使用箭头函数
  4. 事件中使用 bind 绑定
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
40
41
42
43
44
45
46
47
48
class Hello extends React.Component {
constructor(props) {
//初始化props
super(props);
//初始化状态
this.state = {
name: "Tom",
age: 12,
};

//或者在class内部通过bind绑定this
//this.updateUser = this.updateUser.bind(this)
}
//数据更新
updateUser = () => {
//使用箭头函数将this指向当前实例
this.setState({
name: "Jack",
age: 13,
});
};

//外部就可以拿到this
//就可以使用普通函数定义
/*updateUser(){
this.setState({
name: 'Jack',
age: 12
})
}
*/

//渲染
render() {
return (
<div>
<h1>Hello,{this.state.name}</h1>
<p>年龄: {this.state.age}</p>
<button onClick={this.updateUser}>更新数据</button>
//或者在此处改为
<button onClick={() => this.updateUser()}>更新数据</button>
<button onClick={this.updateUser.bind(this)}>更新数据</button>
</div>
);
}
}

ReactDOM.render(<Hello />, document.getElementById("app"));

条件判断

通常使用三元运算符

1
return <div>{isLogin ? <Login /> : <Logout />}</div>;

组件通信(组件传值)

子组件获取父组件的值 Props

通过 Props 可以快捷的拿到父组件的值.

  1. 在子组件上添加属性名和数据
1
<classDemo name="Tom"></classDemo>>
  1. 在 class 中使用 Props
1
2
3
4
5
6
constructor(props){
super(props)
this.state = {
defaultText: "默认文字"
}
}
  1. 通过this.props.父组件绑定的属性名
1
<p>{this.props.name}</p>

子组件传递父组件

通过 props 传递一个函数,当子组件需要改变父组件的值时,通过this.props.[自定义函数]执行回调.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//父组件
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
childText: "123",
};
}

onPropChange = (val) => {
this.setState({
childText: val,
});
};
render() {
return (
<div>
<p>{this.state.childText}</p>
<ClassDemo onChange={this.onPropChange}></ClassDemo>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ClassDemo extends React.Component{
constructor(props){
super(props)
this.state = {
defaultText: '默认文字'
}
}
changeText = ()=>{
this.props.onChange('321')
}
render(){
return (
<div className="App">
<button onClick={ this.changeText }>修改文本</button>
</div>>
)
}
}

列表渲染(循环)

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
class List extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [1, 2, 3, 4, 5],
};
}
render() {
const arr = this.state.list;
const listItem = [];
//使用map方法遍历
//将初始值加上li标签填入新的list中
arr.map((item) => {
listItem.push(<li>{item}</li>);
});
//使用return渲染出新的listItem
return (
<div>
<ul>{listItem}</ul>
</div>
);
}
}

ReactDOM.render(<List />, document.getElementById("app"));

Forms 表单应用

数据发生变化后需要使用 this.setSate 进行更改数据,否则不会生效

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
40
41
42
43
44
45
46
47
48
49
class Forms extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [],
val: "",
};
}
handleChange = (event) => {
// 错误写法 val = event.target.value
//数据发生变化后需要使用this.setSate进行更改数据
this.setState({
val: event.target.value,
});
};
handleAdd = () => {
//从state中获取val和list
const { val, list } = this.state;
//将输入的新值添加到list中
list.push(val);
//重新赋值state
this.setState({
list,
});
};
render() {
const arr = this.state.list;
const listItem = [];
arr.map((item, index) => {
listItem.push(<li key={index}>{item}</li>);
});

return (
<div>
<div>
<input
type="text"
value={this.state.val}
onChange={this.handleChange}
/>
<button onClick={this.handleAdd}>点击添加</button>
</div>
<ul>{listItem}</ul>
</div>
);
}
}

ReactDOM.render(<Form />, document.getElementById("app"));

受控组件和非受控组件

受控组件: React 控制输入的表单元素

实例: 留言框

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
40
41
42
//CommentBox.js
import React from "react";

class CommentBox extends React.Component {
constructor(props) {
super(props);
this.state = {
value: "",
};
//如果不使用bind绑定,可以在函数声明时使用箭头函数方法
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({
value: event.target.value,
});
}
handleSubmit(event) {
alert(this.state.value);
event.preventDefault();
}

render() {
return (
<form onSubmit={this.handleSubmit}>
<div>
<label>留言内容</label>
<input
type="text"
placeholder="请输入内容"
onChange={this.handlechange}
value={this.state.value}
/>
</div>
<button type="submit">留言</button>
</form>
);
}
}

export default CommonBox;

非受控组件写法

将真实数据保存在 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
27
28
29
30
31
32
33
//CommentBox.js
import React from "react";

class CommentBox extends React.Component {
constructor(props) {
super(props);
this.handleSubmit = this.handleSubmit.bind(this);
}

handleSubmit(event) {
alert(this.textInput.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<div>
<label>留言内容</label>
<input
type="text"
placeholder="请输入内容"
ref={(textInput) => {
this.textInput = textInput;
}}
/>
</div>
<button type="submit">留言</button>
</form>
);
}
}

export default CommonBox;


title: this 他喵的到底是什么 date: 2020-07-09 15:20:43

tags:

原文地址:WTF is this - Understanding the this keyword, call, apply, and bind in JavaScript

原文作者:Tyler McGinnis

译文出自:掘金翻译计划

本文永久链接:github.com/xitu/gold-m…

译者:CoolRice

校对者:周家未

在深入了解 JavaScript 中的 this 关键字之前,有必要先退一步,看一下为什么 this 关键字很重要。this 允许复用函数时使用不同的上下文。换句话说,“this” 关键字允许在调用函数或方法时决定哪个对象应该是焦点。 之后讨论的所有东西都是基于这个理念。我们希望能够在不同的上下文或在不同的对象中复用函数或方法。

我们要关注的第一件事是如何判断 this 关键字的引用。当你试图回答这个问题时,你需要问自己的第一个也是最重要的问题是“这个函数在哪里被调用?”。判断 this 引用什么的 唯一 方法就是看使用 this 关键字的这个方法在哪里被调用的。

用一个你已经十分熟悉的例子来展示这一点,比如我们有一个 greet 方法,它接受一个名字参数并显示有欢迎消息的警告框。

1
2
3
function greet(name) {
alert(`Hello, my name is ${name}`);
}

如果我问你 greet 会具体警告什么内容,你会怎样回答?只给出函数定义是不可能知道答案的。为了知道 name 是什么,你必须看看 greet 函数的调用过程。

1
greet("Tyler");

判断 this 关键字引用什么也是同样的道理,你甚至可以把 this 当成一个普通的函数参数对待 — 它会随着函数调用方式的变化而变化。

现在我们知道为了判断 this 的引用必须先看函数的定义,在实际地查看函数定义时,我们设立了四条规则来查找引用,它们是

  1. 隐式绑定
  2. 显式绑定
  3. new 绑定
  4. window 绑定

隐式绑定

请记住,这里的目标是查看使用 this 关键字的函数定义,并判断 this 的指向。执行绑定的第一个也是最常见的规则称为 隐式绑定。80% 的情况下它会告诉你 this 关键字引用的是什么。

假如我们有一个这样的对象

1
2
3
4
5
6
7
const user = {
name: "Tyler",
age: 27,
greet() {
alert(`Hello, my name is ${this.name}`);
},
};

现在,如果你要调用 user 对象上的 greet 方法,你会用到点号。

1
user.greet();

这就把我们带到隐式绑定规则的主要关键点。为了判断 this 关键字的引用,函数被调用时先看一看点号左侧。如果有“点”就查看点左侧的对象,这个对象就是 this 的引用。

在上面的例子中,user 在“点号左侧”意味着 this 引用了 user 对象。所以就好像 在 greet 方法的内部 JavaScript 解释器把 this 变成了 user。

1
2
3
4
greet() {
// alert(`Hello, my name is ${this.name}`)
alert(`Hello, my name is ${user.name}`) // Tyler
}

我们来看一个类似但稍微高级点的例子。现在,我们的对象不仅要拥有 name、age 和 greet 属性,还要被添加一个 mother 属性,并且此属性也拥有 name 和 greet 属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
const user = {
name: "Tyler",
age: 27,
greet() {
alert(`Hello, my name is ${this.name}`);
},
mother: {
name: "Stacey",
greet() {
alert(`Hello, my name is ${this.name}`);
},
},
};

现在问题变成下面的每个函数调用会警告什么?

1
2
user.greet();
user.mother.greet();

每当判断 this 的引用时,我们都需要查看调用过程,并确认“点的左侧”是什么。第一个调用,user 在点左侧意味着 this 将引用 user。第二次调用中,mother 在点的左侧意味着 this 引用 mother。

1
2
user.greet(); // Tyler
user.mother.greet(); // Stacey

如前所述,大约有 80% 的情况下在“点的左侧”都会有一个对象。这就是为什么在判断 this 指向时“查看点的左侧”是你要做的第一件事。但是,如果没有点呢?这就为我们引出了下一条规则

显式绑定

如果 greet 函数不是 user 对象的函数,只是一个独立的函数。

1
2
3
4
5
6
7
8
function greet() {
alert(`Hello, my name is ${this.name}`);
}

const user = {
name: "Tyler",
age: 27,
};

我们知道为了判断 this 的引用我们首先必须查看这个函数的调用位置。现在就引出了一个问题,我们怎样能让 greet 方法调用的时候将 this 指向 user 对象?。我们不能再像之前那样简单的使用 user.greet(),因为 user 并没有 greet 方法。在 JavaScript 中,每个函数都包含了一个能让你恰好解决这个问题的方法,这个方法的名字叫做 call。

call是每个函数都有的一个方法,它允许你在调用函数时为函数指定上下文。

考虑到这一点,用下面的代码可以在调用 greet 时用 user 做上下文。

1
greet.call(user);

再强调一遍,call 是每个函数都有的一个属性,并且传递给它的第一个参数会作为函数被调用时的上下文。换句话说,this 将会指向传递给 call 的第一个参数。

这就是第 2 条规则的基础(显示绑定),因为我们明确地(使用 .call)指定了 this 的引用。

现在让我们对 greet 方法做一点小小的改动。假如我们想传一些参数呢?不仅提示他们的名字,还要提示他们知道的语言。就像下面这样

1
2
3
4
5
function greet(lang1, lang2, lang3) {
alert(
`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`
);
}

现在为了将这些参数传递给使用 .call 调用的函数,你需要在指定上下文(第一个参数)后一个一个地传入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function greet(lang1, lang2, lang3) {
alert(
`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`
);
}

const user = {
name: "Tyler",
age: 27,
};

const languages = ["JavaScript", "Ruby", "Python"];

greet.call(user, languages[0], languages[1], languages[2]);

方法奏效,它显示了如何将参数传递给使用 .call 调用的函数。不过你可能注意到,必须一个一个传递 languages 数组的元素,这样有些恼人。如果我们可以把整个数组作为第二个参数并让 JavaScript 为我们自动展开就好了。有个好消息,这就是 .apply 干的事情。.apply.call 本质相同,但不是一个一个传递参数,你可以用数组传参而且 .apply 会在函数中为你自动展开。

那么现在用 .apply,我们的代码可以改为下面这个,其他一切都保持不变。

1
2
3
4
const languages = ["JavaScript", "Ruby", "Python"];

// greet.call(user, languages[0], languages[1], languages[2])
greet.apply(user, languages);

到目前为止,我们学习了关于 .call.apply 的“显式绑定”规则,用此规则调用的方法可以让你指定 this 在方法内的指向。关于这个规则的最后一个部分是 .bind.bind.call 完全相同,除了不会立刻调用函数,而是返回一个能以后调用的新函数。因此,如果我们看看之前所写的代码,换用 .bind,它看起来就像这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function greet(lang1, lang2, lang3) {
alert(
`Hello, my name is ${this.name} and I know ${lang1}, ${lang2}, and ${lang3}`
);
}

const user = {
name: "Tyler",
age: 27,
};

const languages = ["JavaScript", "Ruby", "Python"];

const newFn = greet.bind(user, languages[0], languages[1], languages[2]);
newFn(); // alerts "Hello, my name is Tyler and I know JavaScript, Ruby, and Python"

new 绑定

第三条判断 this 引用的规则是 new 绑定。若你不熟悉 JavaScript 中的 new 关键字,其实每当用 new 调用函数时,JavaScript 解释器都会在底层创建一个全新的对象并把这个对象当做 this。如果用 new 调用一个函数,this 会自然地引用解释器创建的新对象。

1
2
3
4
5
6
7
8
9
10
11
function User(name, age) {
/*
JavaScript 会在底层创建一个新对象 `this`,它会代理不在 User 原型链上的属性。
如果一个函数用 new 关键字调用,this 就会指向解释器创建的新对象。
*/

this.name = name;
this.age = age;
}

const me = new User("Tyler", 27);

window 绑定

假如我们有下面这段代码

1
2
3
4
5
6
7
8
function sayAge() {
console.log(`My age is ${this.age}`);
}

const user = {
name: "Tyler",
age: 27,
};

如前所述,如果你想用 user 做上下文调用 sayAge,你可以使用 .call、.apply 或 .bind。但如果我们没有用这些方法,而是直接和平时一样直接调用 sayAge 会发生什么呢?

1
sayAge(); // My age is undefined

不出意外,你会得到 My name is undefined,因为 this.age 是 undefined。事情开始变得神奇了。实际上这是因为点的左侧没有任何东西,我们也没有用 .call、.apply、.bind 或者 new 关键字,JavaScript 会默认 this 指向 window 对象。这意味着如果我们向 window 对象添加 age 属性并再次调用 sayAge 方法,this.age 将不再是 undefined 并且变成 window 对象的 age 属性值。不相信?让我们运行这段代码

1
2
3
4
5
window.age = 27;

function sayAge() {
console.log(`My age is ${this.age}`);
}

非常神奇,不是吗?这就是第 4 条规则为什么是 window 绑定 的原因。如果其它规则都没满足,JavaScript 就会默认 this 指向 window 对象。

在 ES5 添加的 严格模式 中,JavaScript 不会默认 this 指向 window 对象,而会正确地把 this 保持为 undefined。

1
2
3
4
5
6
7
8
9
"use strict";

window.age = 27;

function sayAge() {
console.log(`My age is ${this.age}`);
}

sayAge(); // TypeError: Cannot read property 'age' of undefined

因此,将所有规则付诸实践,每当我在函数内部看到 this 关键字时,这些就是我为了判断它的引用而采取的步骤。

  1. 查看函数在哪被调用。
  2. 点左侧有没有对象?如果有,它就是 “this” 的引用。如果没有,继续第 3 步。
  3. 该函数是不是用 “call”、“apply” 或者 “bind” 调用的?如果是,它会显式地指明 “this” 的引用。如果不是,继续第 4 步。
  4. 该函数是不是用 “new” 调用的?如果是,“this” 指向的就是 JavaScript 解释器新创建的对象。如果不是,继续第 5 步。
  5. 是否在“严格模式”下?如果是,“this” 就是 undefined,如果不是,继续第 6 步。
  6. JavaScript 很奇怪,“this” 会指向 “window” 对象。

class 中 this 的绑定

前景提要:当我们打印typeof Cat可知是函数类型,ES6 中的 class 类其实只是个语法糖,皆可以用 ES5 来实现。由构造函数 Cat 创建的实例 cat 是一个对象。在初始化 cat 实例的时候,在 constructor 中就会把 this 上的属性挂载到实例对象上面。

另外牢记,this 的指向与作用域无关,与调用执行函数时的上下文相关。

示例一:

1
2
3
4
5
6
7
8
9
10
11
class Cat {
constructor(name, age) {
this.name = name;
}
run() {
console.log("run", this);
}
}
let cat = new Cat("米粒", "5个月");
cat.name; // '米粒'
cat.run(); // run Cat {name: "米粒"}

当调用cat.run()的时候,当前上下文是 cat,所以其 this 指向的是 cat 这个实例。

示例二:

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
class Cat {
constructor(name, age) {
this.name = name;
this.jump = this.jump.bind(this);
this.drink = () => {
console.log("drink", this);
};
}
run() {
console.log("run", this);
}
jump() {
console.log("jump", this);
}
static go() {
console.log("go", this);
}
}

Cat.prototype.walk = () => {
console.log("walk", this);
};

let cat = new Cat("米粒", "5个月");
let run = cat.run;
let jump = cat.jump;
let go = Cat.go;
let walk = cat.walk;
let drink = cat.drink;

run(); // run undefined
jump(); // jump Cat {name: "米粒", jump: ƒ}
Cat.go(); // go class Cat {}
go(); // go undefined
cat.walk(); // walk Window
walk(); // walk Window
cat.drink(); // drink Cat {name: "米粒", jump: ƒ, drink: ƒ}
drink(); // drink Cat {name: "米粒", jump: ƒ, drink: ƒ}

先看 run 方法:当把实例中的方法赋值给一个变量,但是只是赋予了方法的引用,所以当变量在执行方法的时候,其实改变了方法的执行时的上下文。原来执行的上下文是实例 cat,后来赋值之后再执行,上下文就变成了全局,this 默认绑定。class 中使用的是严格模式,在该模式下,全局的 this 默认绑定的是 undefined,不是在严格模式下的时候,若在浏览器中执行,则 this 默认绑定 window。

jump 方法:因为在构造函数执行的时候,显式绑定了 jump 执行的上下文——cat 实例。由文章开头 this 绑定的优先级可知,显式绑定>默认绑定。所以 jump 的执行上下文依然是 cat 实例

go 方法:go 方法使用静态方法定义,无法共享个实例 cat,只能在构造函数 Cat 上直接调用。

walk 与 drink 方法:这两个方法是用箭头函数定义的。箭头函数的 this 是在定义函数时绑定的,不是在执行过程中绑定的。简单的说,函数在定义时,this 就继承了定义函数的对象。

所以 walk 是在Cat.prototype.walk时定义的,此时的 this 指向是 window。无论之后赋值给哪个变量,也只是函数的引用,所以其 this 还是 window。

同理,drink 在定义的时候,this 指向的是该构造函数。

在 class 中 ,this 默认指向类的实例。但是,如果将这个方法提取出来单独使用,this 会指向该方法运行时所在的环境。

那么问题来了,既然是全局环境的话,那么此时的 this 应该是输出 window,但是却不是 window 而是 undefined。

那么问题出在哪?

其实,就是在 class 中,定义类的时候默认使用的是严格模式。

所以在严格模式下,由于 this 不能指向全局变量,因此这里就变成了 undefined。


title: ES6

date: 2019-09-05 13:59:32

tags: javascript

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • JS # 一级分类
  • ES6 # 二级分类

ES6 常见语法

let 和 const

let定义局部变量

var定义全局变量.用var定义的全部变量,有时候会污染整个 js 的作用域。

const定义常量

在全局作用域下使用letconst声明变量,变量并不会挂载到window

  1. var 可声明前置(变量提升)

变量会提升,函数也会提升,并且函数优先于变量提升,函数提升会把整个函数提到作用域顶部,变量提升只会把声明提上去.

1
2
3
a = 3;
var a;
var a = 4;
  1. let 不可声明前置

暂时性死区:不能在变量声明之前使用变量

1
2
a = 3; //报错
let a;
  1. let 不可重复声明
1
2
3
let a = 3;
let a = 4; //报错
var a = 5; //报错
  1. 存在块级作用域
1
2
3
4
for (let i = 0; i < 3; i++) {
console.log(i); //块级作用域只在大括号内,出了大括号,i并没有声明,会报错
}
console.log(i); //报错

暂时性死区: 在 let 声明变量之前都是该变量的死区,在死区内该变量不可使用.不能被声明也不能被获取.

  1. const 声明的常量不可改变
1
2
3
4
5
6
const a = 1;
a = 2; //报错

const obj = { a: 1 };
obj.a = 2; //正常
obj = { a: 2 }; //报错

const 对象等于引用类型,obj 存的是{a: 1}的地址,里面的东西改变并不影响地址.而obj = {a: 2}是赋给一个新的地址,发生了改变,会报错.

  1. 适用于let同样适用于const

letconst作用基本一致,const声明的变量不能再次赋值

ES6 可以大量使用let,如果认定模块不改变,可以使用const

解构赋值

关于数组的解构赋值

1
2
3
4
5
6
7
8
9
let [a, b, c] = [1, 2, 3];
console.log(a, b, c);

let [a, [b], c] = [2, [3], 4];
a; //2
b; //3
c; //4

let [a] = 1; //报错,因为[a]是一个数组,把值赋值给数组就报错了
  1. 默认值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let [a, b = 2] = [3]
a // 3
b // 2,因为b没有对应值,所以是undefined,使用默认值

let [a, b = 2] = [3, 4]
a //3
b //4
数组对应对值有没有?如果没有(数组对没有指undefined)就使用默认值,如果有就使用对应值

let [a=2, b=3] = [undefined, null]
a //2
b //null
let [a=1, b=a] = [2]
a //2
b //2,因为b没有对应值,所以是undefined,使用默认值,此时a的默认值为2
  1. 对象的解构赋值

前置知识

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let [name, age] = ["hunger", 3];
let p1 = { name, age };
//等同于
let p2 = { name: name, age: age };
解构范例;

let { name, age } = { name: "jirengu", age: 4 };
name; //‘jirengu’
age; //4
以上代码等同于;

let name;
let age;
({ name: name, age: age } = { name: "jirengu", age: 4 });
  1. 默认值
1
2
3
let { x, y = 5 } = { x: 1 };
x; //1
y; //5
  1. 函数解构
1
2
3
4
5
6
7
8
9
10
11
function add([x = 1, y = 2]) {
return x + y;
}
add(); //Error
add([2]); //4
add([3, 4]); //7

function sum({ x, y } = { x: 0, y: 0 }, { a = 1, b = 1 }) {
return [x + a, y + b];
}
sum({ x: 1, y: 2 }, { a: 2 }); //[3, 3]
  1. 作用
1
2
3
4
5
6
7
let [x, y] = [1, 2];
[x, y] = [y, x]
x //2
y // 1
function ajax({url, type=‘GET’}){
}
ajax({url: ‘http://localhost:3000/getData’})

字符串,数组,对象

字符串

  1. 多行字符串
1
2
3
4
5
let str = `
Hi,
This is jirengu.com.
You can study frontend here.
`;
  1. 字符串模板
1
2
3
4
5
6
7
let website = "jirengucom";
let who = "You";
let str = `Hi
This is ${website}.
${who} can study frontend here
`;
//${},可以替换变量
1
2
3
4
5
var data = [1, 2, 3, 4];
var liArr = data.map((v) => `<li>${v}</li>`).join("");
var html = `<ul>${liArr}</ul>`;
console.log(html);
//"<ul><li>1</li><li>2</li><li>3</li><li>4</li></ul>"

数组

  1. 扩展运算符...

主要作用是将数组或对象进行展开.

对象中的扩展运算符(…)用于取出参数对象中的所有可遍历属性,拷贝到当前对象之中

1
2
3
4
5
6
7
var a = [1, 2];
console.log(...a); // 1, 2,这三个点相当于脱壳,把数组的壳拿掉
var b = [...a, 3];
b; // [1, 2, 3]

var c = b.concat([4, 5]); //b拼接[4,5]变成[1,2,3,4,5]
var d = [...b, 4, 5]; //直接把脱壳后的1,2,3放入数组中
  1. 函数参数的扩展
1
2
3
4
5
6
7
8
function sort(...arr) {
console.log(arr.sort());
}
sort(3, 1, 5); //[1, 3, 5]
function max(arr) {
return Math.max(...arr);
}
max([3, 4, 1]); // 4
  1. 类数组对象转数组
1
2
3
4
5
6
7
let ps = document.querySelectorAll("p");
Array.from(ps).forEach((p) => {
console.log(p.innerText);
});
[...ps].forEach((p) => {
console.log(p.innerText);
});

函数

  1. 默认值
1
2
3
4
5
function sayHi(name = "jirengu") {
console.log(`hi, ${name}`);
}
sayHi();
sayHi("ruoyu");
1
2
3
4
function fetch(url, { body = "", method = "GET", headers = {} } = {}) {
console.log(method);
}
fetch("http://example.com"); //如果没有定义那么就是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
//ex1
function m1({ x = 0, y = 0 } = {}) {
return [x, y];
}

//ex2
function m2({ x, y } = { x: 0, y: 0 }) {
return [x, y];
}

// 函数没有参数的情况
m1(); // [0, 0]
m2(); // [0, 0]

// x 和 y 都有值的情况
m1({ x: 3, y: 8 }); // [3, 8]
m2({ x: 3, y: 8 }); // [3, 8]

// x 有值,y 无值的情况
m1({ x: 3 }); // [3, 0]
m2({ x: 3 }); // [3, undefined]

// x 和 y 都无值的情况
m1({}); // [0, 0];把{}赋给(x = 0,y = 0)
m2({}); // [undefined, undefined];把{}赋给{x,y},也就是把undefined, undefined赋给{x,y}

m1({ z: 3 }); // [0, 0]
m2({ z: 3 }); // [undefined, undefined]

ex1:调用函数需要你传递一个对象,如果你没传对象就用默认值对象{},默认值对象里面都是 undefined, 所以属性使用初始值

ex2:参数需要是一个对象,如果没传对象,就用默认值对象{ x: 0, y: 0 }如果传了对象,就使用你传递的对象

  1. 箭头函数

箭头后面的内容,就相当于return的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var f = (v) => v + 1;
//等价于
var f = function (v) {
return v + 1;
};

var f = () => 5;
// 等同于
var f = function () {
return 5;
};

var sum = (num1, num2) => num1 + num2; //适用于立刻返回,即把代码写成一行的情况
// 等同于
var sum = function (num1, num2) {
return num1 + num2;
};
1
2
3
4
//数组的平方
var arr = [1, 2, 3];
var arr2 = arr.map((v) => v * v);
arr2; //[1, 4, 9]
  1. 箭头函数里面的 this

1.箭头函数会捕获其所在上下文的 this 值作为自己的 this 值,自己本身并没有 this 值.

2.箭头函数的 this 永远指向其上下文的 this,任何方法都改变不了其指向,如 call,bind,apply.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ES6
function foo() {
setTimeout(() => {
console.log("id:", this.id);
}, 100);
}

// 等同于如下ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log("id:", _this.id);
}, 100);
}

对象

1
2
3
var name = "jirengu";
var age = 3;
var people = { name, age }; //{name:'jirengu', age:3}
1
2
3
4
5
6
7
let app = {
selector: "#app",
//函数简写
init() {},
bind() {},
};
app.init();

类和继承

  1. 构造函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

sayHello() {
console.log(`hello, ${this.name}, i am ${this.age} years old`);
}
}
等价于;

function Person(name, age) {
this.name = name;
this.age = age;
}

Person.prototype.sayHello = function () {
console.log(`hello, ${this.name}, i am ${this.age} years old`);
};

var p = new Person("hunger", 2);

静态方法

静态方法调用直接在类上进行,不能在类的实例上调用。

下面的例子说明了这几点:

静态方法如何在类上实现。

具有静态成员的类,可以被子类化 。

什么情况下静态方法可以调用,什么情况下不能调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Tripple {
static tripple(n = 1) {
return n * 3;
}
}

class BiggerTripple extends Tripple {
static tripple(n) {
return super.tripple(n) * super.tripple(n);
}
}

console.log(Tripple.tripple()); // 3
console.log(Tripple.tripple(6)); // 18

let tp = new Tripple();

console.log(BiggerTripple.tripple(3)); // 81(不会受父类实例化的影响)
//super继承父类的方法super.tripple(n) = 3n,BiggerTripple.tripple(n) = (3n)^2.当n=3,输出81
console.log(tp.tripple()); // 'tp.tripple 不是一个函数'.说明只能在类上实现.
  1. 继承

js 中并不存在类,class只是语法糖,本质是函数

class 继承的核心在于使用extends表明继承自哪个父类,并在子类构造函数中必须调用super

通过super获取父类的属性.

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
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}

sayHello() {
//写的函数会直接放到原型上
console.log(`hello, ${this.name}, i am ${this.age} years old`);
}
}
class Student extends Person {
//这里的constructor是Student的构造函数,内含三个参数
constructor(name, age, score) {
//这里的super是继承自父类的参数
super(name, age);
this.score = score;
}

sayScore() {
console.log(
`hello, ${this.name}, i am ${this.age} years old, i get ${this.score}`
);
}
}

模块化

为什么使用模块化?

  1. 解决命名冲突
  2. 提供复用性
  3. 提高代码可维护性

写法 1

1
2
3
4
5
6
7
// profile.js
export var firstName = "Michael";
export var lastName = "Jackson";
export var year = 1958;
//useage.js
import { firstName, lastName, year } from "./profile";
console.log(firstName);

写法 2

1
2
3
4
5
6
7
8
var firstName = "Michael";
var lastName = "Jackson";
var year = 1958;

export { firstName, lastName, year };
//useage.js
import { firstName, lastName, year } from "./profile";
console.log(firstName);

写法 3

1
2
3
4
5
6
//helper.js
export function getName() {}
export function getYear() {}
//main.js
import { getName, getYear } from "./helper";
getName();

写法 4

1
2
3
4
5
6
7
//helper.js
function getName() {}
function getYear() {}
export { getName, getYear };
//main.js
import { getName, getYear } from "./helper";
getName();

写法 5

1
2
3
4
5
6
7
// export-default.js
export default function () {
console.log("foo");
}
// import-default.js
import getName from "./export-default";
getName();


title: React 进阶 date: 2020-05-09 11:43:23

tags: React

Context

  • props 属性是由上到下单向传递的
  • Context 提供了在组件中共享此类值的方法

案例:主题切换

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
//App.js
import React from "react";

const themes = {
light: {
classnames: "btn btn-primary",
bgColor: "#eeeeee",
color: "#000",
},
dark: {
classnames: "btn btn-light",
bgColor: "#222222",
color: "#fff",
},
};
class App extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div calssName="App">
<header className="App-header">
<a href="#theme-switcher" className="btn btn-light">
浅色主题
</a>
<a href="#theme-switcher" className="btn btn-secondary">
深色色主题
</a>
</header>
</div>
);
}
}
export default App;

新建 Context 文件,Context 是 object,不是 component.在 src 下.

1
2
3
4
5
6
//src/theme-context.js
import React from "react";

const ThemeContext = React.creatContext();

export default ThemeContext;

ThemeContext 返回两个值,一个是 Provider,一个是 Consumer.

ThemeContext.Provider 包裹根组件

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
40
41
42
43
44
45
46
47
48
49
//App.js
import React from 'react'
import ThemeContext from '../theme-context'

const themes = {
light: {
classnames: 'btn btn-primary',
bgColor: '#eeeeee',
color: '#000'
},
dark: {
classnames: 'btn btn-light',
bgColor: '#222222',
color: '#fff'
}
}
class App extends Component {
constructor(props){
super(props)
this.state = {
theme: 'light'
}
this.changeTheme = this.changeTheme.bind(this)
}
change(theme){
this.setState({
theme
})
}
render(){
return (
<ThemeContext.Provider value={themes.light}>
<div calssName="App">
<header className="App-header">
<a href="#theme-switcher"
className="btn btn-light"
onClick={() => {this.changeTheme('light')}}
>浅色主题</a>
<a href="#theme-switcher"
className="btn btn-secondary"
onClick={() => {this.changeTheme('dark')}
>深色色主题</a>
</header>
</div>
</ThemeContext.Provider>
)
}
}
export default App

新建组件 themeBar.js 组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//component/themeBar.js
import React from "react";
import ThemeContext from "../theme-context";

const ThemeBar = () => {
return (
<themeContext.Consumer>
{(theme) => {
return (
<div
className="alert mt-5"
style={{ backgroundColor: theme.bgColor, color: theme.color }}
>
样式区域
<button className={theme.classname}>样式按钮</button>
</div>
);
}}
</themeContext.Consumer>
);
};
export default ThemeBar;


title: React-CommentBoxdate: 2020-05-08 11:44:09

tags: React

综合实例-留言本

状态提升和单向数据流

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
//App.js
//将状态提升至App.js
import React from "react";
import CommentBox from "./components/CommentBox";
import CommentList from "./components/Commentlist";

class App extends Component {
constructor(props) {
super(props);
this.state = {
commments: ["this id my first reply"],
};
this.addComment = this.addComment.bind(this);
}

addComment(comment) {
this.setState({
comments: [...this.state.comment, comment],
});
}

render() {
const { comments } = this.state;
return (
<div className="App">
<CommentList comments={comments} />
<CommentBox
commentsLength={comments.length}
onAddComment={this.addComment}
/>
</div>
);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//CommentList.js
import React from "react";

const CommentList = ({ comments }) => {
return (
<div className="comment-list-component">
<label>评论列表</label>
<ul className="list-group">
{comments.map((comment, index) => (
<li key={index} className="list-group-item">
{comment}
</li>
))}
</ul>
</div>
);
};

export default CommentList;
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
//CommentBox.js
import React from 'react'

class CommentBox from React.Component {
constructor(props){
super(props)
this.handleSubmit = this.handleSubmit.bind(this)
}
handleSubmit(event){
alert(this.textInput.value)
event.preventDefault()
}

render(){
return (
<form onSubmit={this.handleSumit}>
<div>
<label>留言内容</label>
<input
type="text"
className="form-control"
placeholder="请输入评论"
ref={(textInput) => {this.textInput = textInput}}
/>
</div>
<button type="submit">
留言
</button>
<p>已有{this.props.commentLength = {this}}条评论</p>
</form>
)
}
}

总结

React 开发思想以及和其他思想的异同

  • 状态提升
  • 自上而下的数据流
  • 和双向绑定的区别


title: taichidate: 2019-12-20 16:14:28

tags: android

what is TaiChi?

TaiChi is a framework that can use Xposed modules. Modules can change the behavior of systems and applications through it. TaiChi  App can run without needing to root, unlock the bootloader; and it supports Android 5.0 ~ 10.

In simple terms, TaiChi is an Xposed-like framework, which can load Xposed modules, modify the system and APP, intercept methods, and execute hook logic.

Relationship with Xposed

TaiChi is an Xposed-like framework, but it has nothing to do with Xposed itself. The only thing that might be related is that TaiChi is compatible with the Xposed modules. Besides, the two are completely different in design thinking, implementation mechanism, and operation logic.

Here are some unique features of TaiChi:

How to use TaiChi?

TaiChi has two working modes: non-root mode and magisk mode. If you don’t want to unlock the bootloader/flash system images, the non-root mode is perfect for you, if you want more control over the system, you can try magisk mode.

What is the difference between non-root mode and magisk mode?

The only different support is that the magisk mode can modify the system, so it can support more modules; such as Greenify / CorePatch, etc. However, Magisk mode requires unlocking the bootloader and installing Magisk, while non-root mode only requires installing a simple APP (TaiChi).

Non-Root mode

Magisk mode

Although the Non-Root mode of TaiChi does not require unlocking the bootloader and is extremely convenient to use, it has some disadvantages, such as the inability to modify the system and the need to uninstall the original APP. Therefore,Magisk module of TaiChi, which can give TaiChi more powerful functions through Magisk; thus breaking through the limitations of the Non-Root mode.

After you flash in the Magisk Module provided by TaiChi, the TaiChi APP will automatically switch from Non-Root mode to Magisk mode: TaiChi APP + TaiChi Magisk Module = TaiChi Magisk. When Magisk Module of TaiChi is disabled or uninstalled, TaiChi will automatically return to Non-Root mode.

If you want to use magisk mode, please download the latest Magisk Module.

page2

TaiChi module download


title: JS基础

date: 2019-08-19 13:39:26

tags: javascript

categories:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • JS # 一级分类
  • JS基础 # 二级分类

JS 对象

对象,就是一种无序的数据集合,由若干个“键值对”(key-value)构成。

{key: value}是 JS 对象字面量写法

数据类型

number, boolean, string, undefined, null, object, symbol

原始类型,又叫基本类型,值类型,number, string, boolean,undefined,null。指保存在栈内存里的简单数据段

引用类型,对象 object。又可分为狭义的对象,数组,函数,正则。指保存在堆内存中的对象

其中 js 的 number 类型是浮点类型,不是整型.并且浮点类型基于 IEEE 754 标准实现,在使用中会遇到某些 Bug。

NaN 属于 number 类型,NaN表示Not a Number,并且 NaN 不等于自身.

undefined: 表示未定义或不存在.由于没有定义,所以目前没有值

null: 表示无值.即此处的值就是”无”的状态.

typeof

typeof 对于基本类型,除了 null 都可以显示正确的类型

typeof undefined 输出 undefined

typeof []// ‘object’

typeof {} // ‘object’

typeof function(){} //‘function’(这是具体的判断)

typeof null 输出 object

PS:为什么会出现这种情况呢?因为在 JS 的最初版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。

运算符

  1. && (And)
  2. || (or)
  3. ! (not)
1
2
3
a = 1 && 3; //3
a = 1 || 3; //1
!2 * 0; //0

instanceof运算符,判断是否为某一种的实例.

=====,前者先转化类型,后进行判断,后者比较类型和值

三目运算符

condition ? true case : false case

四则运算符

只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。并且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串。

1
2
3
4
5
6
7
8
9
10
11
12
加法拼接两个字符串
console.log("2"+"4") //"24"
加法将其中不是字符串的一方转化为字符串进行拼接
console.log(2+"4") //"24"
只有一个字符串参数时将其转化成数字
console.log(+"4") //4
console.log(+new Date()) //1573972012164
字符串加对象会把对象变成[object Object]
console.log('aa:'+{a:1}) //aa:[object Object]

在参数有对象的情况下调用其valueof或toString,其中valueof优先级更高
console.log(2+new Date()) //"2Sun Nov 17 2019 14:21:50 GMT+0800 (香港标准时间)"

valueof()返回适合该对象类型的原始值。

toString()将该对象的原始值以字符串形式返回。

这两个方法一般是交由 js 去隐式调用。

在数值运算中,优先调用 valueof().

在字符串运算中,优先调用 toString().

自增运算符: a 或 a

1
2
(a = 1), (b = a++); //a=2,b=1
(a = 1), (b = ++a); //a=2,b=2

无论是先加,还是后加,a 的值计算后一定会加 1;

1
2
(a = 3), (b = 4);
a++ + b; //相当于(a++)+b => 3 + 4 = 7

逗号运算符

取后一个表达式的值.也就是说,前一个会被覆盖.

1
var d = ((a = 1), (b = 2)); // 2

boolean 值

false: undefined null 0 NaN 空字符串

除此之外,其他所有值都转化为 true,包括所有对象.

==比较相等性

相等操作符会先转换操作数(通常称为强制转型),然后比较它们的相等性。

在转换不同的数据类型时,相等操作符遵循下列基本规则:

  1. 如果有一个操作数是布尔值,则在比较相等性之前,将其转换为数值;
  2. 如果一个操作数是字符串,另一个操作数是数值,在比较之前先将字符串转换为数值;
  3. 如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf() 方法,用得到的基本类型值按照前面的规则进行比较;
  4. 如果有一个操作数是 NaN,无论另一个操作数是什么,相等操作符都返回 false;
  5. 如果两个操作数都是对象,则比较它们是不是同一个对象。如果指向同一个对象,则相等操作符返回 true;
  6. 在比较相等性之前,不能将 null 和 undefined 转成其他值。
  7. null 和 undefined 是相等的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[] == [] //false
{} == {} //false
声明引用类型的时候,变量名保存在 js 的栈内存里面,而对应的值保存在堆内存里面
而这个变量在栈内存中实际保存的是:这个值在堆内存中的地址,也就是指针.
如果
a = {}
b = {}
虽然 a 和 b 都保存了一个 Object,但这是两个独立的 Object,它们的地址是不同的.
再结合前面的第5条规则:如果两个对象指向同一个对象,相等操作符返回 true
所以 {} == {} 的结果是 false,同样的, [] == [] 的结果也是 false

[] == ![] //true
按优先级,先判断![],是false. =>[] == false.[]是对象,调用[].toString返回"",false转化成0. =>"" == 0.Number("")是0,那么结果就是true.
{} == !{} //false
与上面类似,不过Number({})返回 NaN
"hello" == true //先转化,后比较.true转化成数字是1,"hello"转化成数字是NaN,二者不相等.
"hello" == false //false转化为数字是0,NaN不等于0.二者也不相等.

JSON 格式(javascript object notation)

  1. 复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象。
  2. 简单类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和 null(不能使用 NaN, Infinity, -Infinity 和 undefined)。
  3. 字符串必须使用双引号表示,不能使用单引号。
  4. 对象的键名必须放在双引号里面。
  5. 数组或对象最后一个成员的后面,不能加逗号。

JSON.stringify

用于将一个值转为字符串。该字符串符合 JSON 格式,并且可以被 JSON.parse 方法还原。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
JSON.stringify("abc"); // ""abc""
JSON.stringify(1); // "1"
JSON.stringify(false); // "false"
JSON.stringify([]); // "[]"
JSON.stringify({}); // "{}"

JSON.stringify([1, "false", false]);
// '[1,"false",false]'

JSON.stringify({ name: "张三" });
// '{"name":"张三"}'
var arr = [undefined, function () {}];
JSON.stringify(arr); // "[null,null]"

JSON.stringify(/foo/); // "{}"
//正则对象会被转成空对象。

上面代码将各种类型的值,转成 JSON 字符串。

需要注意的是,对于原始类型的字符串,转换结果会带双引号。

如果原始对象中,有一个成员的值是undefined、函数或 XML 对象,这个成员会被过滤。

如果数组的成员是undefined、函数或 XML 对象,则这些值被转成null

正则对象会被转成空对象。

JSON.parse()

用于将 JSON 字符串转化成对象。

1
2
3
4
5
6
7
8
JSON.parse("{}"); // {}
JSON.parse("true"); // true
JSON.parse('"foo"'); // "foo"
JSON.parse('[1, 5, "false"]'); // [1, 5, "false"]
JSON.parse("null"); // null

var o = JSON.parse('{"name": "张三"}');
o.name; // 张三

JSON.parse 只能解析字符串,下面这种情况会报错.

1
2
3
4
5
6
var str = { name: "张三" };
var o = JSON.parse(str);
o.name; // 报错
解决方法: var str = '{"name": "张三"}';
或者;
var o = JSON.parse("str");

JavaScript 对象的字面量写法只是长的像 JSON 格式数据,二者属于不同的范畴,JavaScript 对象中很多类型(函数、正则、Date) JSON 格式的规范并不支持,JavaScript 对象的字面量写法更宽松。

写 JavaScript 的基本规范

1.不要在同一行声明多个变量。

2.请使用 ===/!==来比较 true/false 或者数值

3.使用对象字面量替代 new Array 这种形式

4.不要使用全局函数。

5.Switch 语句必须带有 default 分支

6.函数不应该有时候有返回值,有时候没有返回值。

7.For 循环必须使用大括号

8.If 语句必须使用大括号

9.for-in 循环中的变量 应该使用 var 关键字明确限定作用域,从而避免作用域污染。

JS 函数

函数会声明前置

1
2
3
4
foo(); //hello
function foo() {
return "hello";
}

函数表达式不会

1
2
3
4
fn()//报错
var fn() = fnction(){
return "hello"
}
1
2
3
4
5
6
7
8
var str = "一号";
test();
function test() {
console.log(str);
var str = "二号";
console.log(str);
}
console.log(str);

输出

1
2
3
4
5
//考察变量声明和作用域
undefined; //因为打印str时,首先从str所在的作用域找变量str,
//因为有声明前置,var str,但是未赋值,是undefined.所以不再从外部找str
二号;
一号; //函数test内部的str=二号只在函数内部,最后一个str的值是全局下的即"一号"

js 对象查看所有属性

1
2
var obj = { p: 1 };
Object.keys(obj);

删除命令

delete object.p

循环判断

1
2
3
4
5
6
7
8
9
10
switch(判断语句){
case 条件:
输出;
break;
case 条件:
输出;
break;
default:
输出
}

while 循环

1
2
3
4
5
while (expression) {
statement;
}
//如果expression为true,执行,
//循环继续执行直到判断不成立。

do-while 循环

1
2
3
4
do {
statement;
} while (expression);
//先执行,后判断

for 循环

1
2
3
for (var i = 0; i < 5; i++) {
console.log(i);
}

执行步骤如下:

初始化语句: var i=0,只执行第一次

判断语句: i<5

条件改变语句: i++

内部执行: console.log(i)

先执行初始化语句,然后判断条件是否成立,成立就执行内部语句,最后改变条件.重复进行判断.

for-in 循环

1
2
3
4
var arr = [1, 2, 3];
for (var key in arr) {
console.log(arr[key]);
}

break退出本次循环

continue跳过本次循环执行下次循环

引用类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//a,b存放在栈内存中
var a = 1;
var b = 2;
//obj是对象,存放在堆内存中,obj只是一个指针,一个地址
var obj = {
name: "Tom",
sex: "male",
age: 15,
friend: {
name: "Jerry",
age: 10,
},
};
var newobj = {}; //空对象也是引用类型,只不过暂时还没有东西

//基本类型
b = a;
console.log(b); //1,把a赋值给b,b在栈内存中也被改变为1
//引用类型
var obj2 = obj;
//二者都是引用类型,都是把地址指向同一个地方,改变内存中的值,另一个也会改变
obj2.name = "Ann";
console.log(obj.name); //'Ann'
1
2
3
4
var obj3 = { name: "hello" };
var obj4 = { name: "hello" };
obj3 === obj4; //false,
//虽然二者内容看似相同,但是确是不同的内存中,两个地址
1
2
3
4
5
6
function sum() {
console.log("sum..");
}
var sum2 = sum;
sum2(); //'sum..',
//函数同样是引用类型,当赋值给另一个函数时,二者也就指向同一地址

函数的参数传递

1
2
3
4
5
6
7
function inc(n) {
n++;
}
var a = 10;
inc(a);
console.log(a); //10
//inc(a)把a传递进去,先赋值后相加,a=10,inc(a) = 11,a并没有变化还是10
1
2
3
4
5
6
7
function incObj(obj) {
obj.n++;
}
var o = { n: 10 };
incObj(o);
console.log(o); //{n:11}
//incObj(o),o.n++,那么{n:11},堆内存中的数值已经改变,输出o时,o也就变成了{n:11}

深拷贝

1
2
3
4
5
6
7
8
9
var obj = {
name: "hunger",
age: 3,
friends: ["aa", "bb", "cc"],
};

var obj2 = JSON.parse(JSON.stringify(obj));
obj.age = 4;
console.log(obj2.age);

缺点: JSON 不支持函数、引用、undefined、RegExp、Date……

jQuery.extend()

jQuery.extend( [deep], target, object1 [, objectN ] )

其中 deep 为 Boolean 类型,如果是true,则进行深拷贝。

1
2
3
4
5
6
7
8
9
10
11
var target = { a: 1, b: 1 };
var copy1 = { a: 2, b: 2, c: { ca: 21, cb: 22, cc: 23 } };
var copy2 = { c: { ca: 31, cb: 32, cd: 34 } };
var result = $.extend(true, target, copy1, copy2); // 进行深拷贝
console.log(target); // {a: 2, b: 2, c: {ca: 31, cb: 32, cc: 23, cd: 34}}

var target = { a: 1, b: 1 };
var copy1 = { a: 2, b: 2, c: { ca: 21, cb: 22, cc: 23 } };
var copy2 = { c: { ca: 31, cb: 32, cd: 34 } };
var result = $.extend(target, copy1, copy2); // 不进行深拷贝
console.log(target); // {a: 1, b: 1, c: {ca: 31, cb: 32, cd:34}}

递归深拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function clone(object) {
var object2;
if (!(object instanceof Object)) {
return object;
} else if (object instanceof Array) {
object2 = [];
} else if (object instanceof Object) {
object2 = {};
}
//你也可以把 Array Function Object 都当做 Object 来看待
for (let key in object) {
object2[key] = clone(object[key]);
}
return object2;
}

函数

函数返回值 return

通过 return 返回结果,调用 return 后,函数立即中断并返回结果,即使后面还有语句也不再执行.

立即执行函数

将整个函数表达式包含在括号内,最后添加一个括号表示执行.

1
2
3
(function () {
console.log("hello");
})();

命名冲突

当在同一个作用域内定义了名字相同的变量和方法的话,无论其顺序如何,变量的赋值会覆盖方法的赋值.

1
2
3
var fn = 3;
function fn() {}
console.log(fn); //3

当函数执行有命名冲突时,函数执行时载入顺序是变量,函数,参数

1
2
3
4
5
6
7
8
function fn(fn) {
console.log(fn);

var fn = 3;
console.log(fn);
}

fn(10); //10 3

函数作用域(scope)

作用域(scope)指的是变量存在的范围。

在函数内定义的变量不能在函数之外的任何地方访问,因为变量仅仅在该函数的域的内部有定义。相对应的,一个函数可以访问定义在其范围内的任何变量和函数。

词法作用域

创建当前函数所在的作用域.

递归

一个函数可以指向并调用自身。调用自身的函数我们称之为递归函数。在某种意义上说,递归近似于循环。两者都重复执行相同的代码,并且两者都需要一个终止条件(避免无限循环或者无限递归)。

作用域链

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

闭包(closure)

它可以访问外部函数的参数和变量,但是外部函数却不能使用它的参数和变量。

一般情况下使用闭包主要是为了

  1. 封装数据
  2. 暂存数据

面试官想听的版本:

什么是闭包?

由于在 JS 中,变量的作用域属于函数作用域,在函数执行后作用域就会被清理、内存也随之回收,但是由于闭包是建立在一个函数内部的子函数,由于其可访问上级作用域的原因,即使上级函数执行完,作用域也不会随之销毁,这时的子函数——也就是闭包,便拥有了访问上级作用域中的变量的权限,即使上级函数执行完后作用域内的值也不会被销毁。

闭包解决了什么问题?

由于闭包可以缓存上级作用域,那么就使得函数外部打破了“函数作用域”的束缚,可以访问函数内部的变量。以平时使用的 Ajax 成功回调为例,这里其实就是个闭包,由于上述的特性,回调就拥有了整个上级作用域的访问和操作能力,提高了极大的便利。开发者不用去写钩子函数来操作上级函数作用域内部的变量了。

闭包的应用?

闭包随处可见,一个 Ajax 请求的成功回调,一个事件绑定的回调方法,一个 setTimeout 的延时回调,或者一个函数内部返回另一个匿名函数,这些都是闭包。简而言之,无论使用何种方式对函数类型的值进行传递,当函数在别处被调用时都有闭包的身影。

1
2
3
4
5
6
7
8
9
10
11
12
13
function car() {
var speed = 0;
function fn() {
speed++;
console.log(speed);
}
return fn;
}

var speedUp = car();
speedUp(); //1
speedUp(); //2
//第二次执行speedup时,在fn内部,speed此时因为没有释放,还是上次的值1,也就相当于var speed = 1;不用再去上一级作用域找speed的值,可以直接相加输出

如果没有这个闭包,函数执行后,里面 speed 变量就会被清理掉。但我们声明了 fn 这个函数,并把它返回出来赋值给新的变量 speedup。因为 speedup 是全局变量,是一直存在的,故这个 fn 函数就一直存在,speed 变量也不会被清理

1
2
3
4
5
6
7
8
9
10
//演化版
var speedup = (function (speed) {
return function () {
speed++;
console.log(speed);
};
})(3);
speedup(); //4
speedup(); //5
speedup(); //6

经典范例

如下代码输出多少?如果想输出 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]()); // 10

我的理解: fnArr[i]声明的匿名函数的作用域是全局作用域,先遍历,并未执行,生成闭包(只是 for 循环,并没有执行函数,后面没有小括号),遍历结束开始执行,fnArr 结果是 10 个匿名函数f(),全局作用域下 0-9 的fnArr[i]已经变成 10 了

原理解析:for 循环每次执行,都把 function(){ return i} 这个函数赋值给 fnArr[i],但这个函数不执行。

因为 fnArr[3] =function(){ return i};故当我们调用 fnArr3 时,相当于 function(){ return i};这个函数立刻执行,这时 for 循环已经完成,i 已经变成了 10。故输出 10

如果要输出 3,需要如下改造

1
2
3
4
5
6
7
8
9
var fnArr = [];
for (var i = 0; i < 10; i++) {
(function (i) {
fnArr[i] = function () {
return i;
};
})(i); //这样保证遍历i时,函数会立即执行(生成10个闭包,暂存数据)
}
console.log(fnArr[3]()); // 3

我的理解: 从 0 开始遍历,(function(0){ fnArr[0] = function(){return i}})(0),

(function(1){ fnArr[1] = function(){return i}})(1)

(function(2){ fnArr[2] = function(){return i}})(2),

(function(3){ fnArr[3] = function(){return i}})(3),

传入 3,最内部的 function 找 i 的变量,一直找到上一层,返回 3.

1
2
3
4
5
6
7
8
9
var fnArr = [];
for (var i = 0; i < 10; i++) {
fnArr[i] = (function (j) {
return function () {
return j;
};
})(i); //生成10个闭包,暂存数据,j换成i也是一样
}
console.log(fnArr[3]()); // 3

如下代码输出什么?

1
2
3
4
5
6
for (var i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
//输出5个5

如何连续输出 0,1,2,3,4

1
2
3
4
5
6
7
8
for (var i = 0; i < 5; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 0);
})(i);
}
//0,1,2,3,4
1
2
3
4
5
6
7
8
//第二种方法
//把var换成let
for (let i = 0; i < 5; i++) {
setTimeout(function () {
console.log(i);
}, 0);
}
//0,1,2,3,4
1
2
3
4
5
6
7
8
9
10
11
12
var an = function (n) {
for (var i = 0; i < 2; i++) {
n++;
console.log(n); //2,3. 遍历两次,内部就打印两次
console.log(i); //0,1. 遍历两次,内部就打印两次
}
console.log(n); //3. 打印最终结果
console.log(i); //2. 打印最终结果.
//如果换成let,只有这里报错,因为i只在for循环体内被定义
};

var g = an(1);

函数柯里化

只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数.

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

1
2
3
4
5
6
7
8
9
console.log(sum(1)(2)); //3
console.log(sum(5)(-1)); //4

//解答
function sum(a) {
return function (b) {
return a + b;
};
}

字符串

1
2
str.charAt(0); //获取第一个字符
str.charCodeAt(0); //获取字符对应的ASC码

字符串截取

1
2
3
4
var str = "hello";
str.substr(1, 3); //第一个开始,第二个长度
str.substring(1, 3); //第一个开始,第二个结束
str.slice(1, 3); //同上,允许负参

查找

1
2
3
4
str.search("he"); //返回下标
str.indexOf("he"); //同上
str.replace("he", "you"); //替换
str.match("he"); //匹配

大小写

1
2
str.toUpperCase();
str.toLowerCase(); //本身不变化

去除首尾空格

1
str.trim(); //返回新数组,不影响原数组

split()方法使用指定的分隔符字符串将一个 String 对象分割成字符串数组,以将字符串分隔为子字符串,以确定每个拆分的位置。

找到分隔符后,将其从字符串中删除,并将子字符串的数组返回。如果没有找到或者省略了分隔符,则该数组包含一个由整个字符串组成的元素。如果分隔符为空字符串,则将 str 转换为字符数组。如果分隔符出现在字符串的开始或结尾,或两者都分开,分别以空字符串开头,结尾或两者开始和结束。因此,如果字符串仅由一个分隔符实例组成,则该数组由两个空字符串组成。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
var str = "The quick brown fox jumps over the lazy dog.";

var words = str.split(" ");
console.log(words[3]);
// expected output: "fox"

var chars = str.split("");
console.log(chars[8]);
// expected output: "k"

var strCopy = str.split();
console.log(strCopy);
// expected output: Array ["The quick brown fox jumps over the lazy dog."]

当字符串为空时,split()返回一个包含一个空字符串的数组,而不是一个空数组,如果字符串和分隔符都是空字符串,则返回一个空数组。

1
2
3
var str = "";
var strEm = str.split();
console.log(strEm); //返回[""]

数组

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
arr.push(newelement) //末尾新增一个
arr.pop() //末尾踢出一个字
arr.shift() //开头拿出一个
arr.unshift() //开头新增一个
arr.splice(index,替换几个,替换内容) //原数组变
//返回由删除元素组成的新数组
arr.slice(start,end) //原数组不变
arr.join("-") //将数组变成字符串并用-连接
arr.reverse() //数组反转
arr.sort(v1-v2) //没有参数按顺序排列,但是会出现10,11,7,8,9排列,
//有参数设置比较函数,函数结果v1-v2如果大于0,两两替换位置。否则,不变
a.concat(array) //拼接,返回数组a和array的集合。原数组不变

Array.isArray(obj) //判断传入的obj是不是数组
a.indexof(2) //判断数组a中是否含有2,返回其索引

a.forEach(fn(element,index,arr){})
//forEach里是一个回调函数,有三个参数,分别是当前元素,索引值,整个数组
a.map(fn(element){})
//与forEach类似,返回新数组,原数组不变,是被一个函数处理后的数组.
//而forEach会修改原数组

a.every(fn(element,index,arr){})
//every是所有函数返回true才会返回true,遇到false终止,并返回false
a.some(fn(element,index,arr){})
//some是存在一个返回true时就终止返回true

a.filter(fn(element){})
//返回新数组,回调函数用于逻辑判断,为true则把当前元素加到返回的数组中
//为false则不加

a.reduce(fn(v1,v2),value)
//将数组元素合成一个值,从索引值最小开始。value表示初始值

a.abs(x) //返回x的绝对值

Array.from(new Set(a)) //数组去重

数组扁平化

1
2
3
4
5
6
7
8
9
10
const flattenDeep = (arr) =>
Array.isArray(arr)
? arr.reduce((a, b) => [...a, ...flattenDeep(b)], [])
: [arr];

flattenDeep([1, [[2], [3, [4]], 5]])[
//flatMap方法,目前该函数在浏览器中还不支持
(1, [2], 3)
].flatMap((v) => v + 1);
// -> [2, 3, 4]

数组去重

双重循环去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function unique(arr){
for(var i=0; i<arr.length; i++){
for(var j=i+1; j<arr.length; j++>{
if(arr[i]===arr[j]){
//如果数组第一个等于第二个
arr.splice(j,1)
//就干掉第j个
j--
//因为数组arr被干掉一个,长度-1,j就相应的-1
}
})
}
return arr
}

new Set 去重

1
2
3
4
5
6
7
8
9
function unique(arr) {
return Array.from(new Set(arr));
}
//简化版
function unique(arr) {
return [...new Set(arr)];
}
//精简版
var unique = (arr) => [...new Set(arr)];

如何将浮点数点左边的数每三位添加一个逗号

如 12000000.11 转化为『12,000,000.11』?

1
2
3
4
5
6
7
8
function commafy(num) {
return (
num &&
num.toString().replace(/(\d)(?=(\d{3})+\.)/g, function ($1, $2) {
return $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
方法一:
var arr = [1,2,3,4,5,6,7,8,9,10];
function randSort1(arr){
for(var i = 0,i < arr.length; i++ ){
var rand = parseInt(Math.random()*len);
var temp = arr[rand];
arr[rand] = arr[i];
arr[i] = temp;
}
return arr;
}
console.log(randSort1(arr));

方法二:
var arr = [1,2,3,4,5,6,7,8,9,10];
function randSort2(arr){
var mixedArray = [];
while(arr.length > 0){
var randomIndex = parseInt(Math.random()*arr.length);
mixedArray.push(arr[randomIndex]);
arr.splice(randomIndex, 1);
}
return mixedArray;
}
console.log(randSort2(arr));

方法三:
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.sort(function(){
return Math.random() - 0.5;
})
console.log(arr);

Math

四舍五入

1
2
Math.round(0.5); //1
Math.round(-1.5); //-1

绝对值

1
Math.abs(-1); //1

最大值

Math.max() 函数返回一组数中的最大值。

参数是value1, value2, ...

是一组数值

1
2
3
Math.max(10, 20,30);
Math.max.call(null,10, 20,30);
Math.max.apply(null,[10, 20,30]);
1
2
3
4
floor返回小于参数值的最大整数.向下取整
Math.floor(-3.2) //-4
ceil返回大于参数值的最小参数,向上取整
Math.ceil(-3.2) //-3

parseInt(值,进制)

1
2
3
parseInt(string, radix);
//将一个字符串 string 转换为 radix 进制的整数,
// radix 为介于2-36之间的数。

parseInt()可以将字符串转为数字,也可以将数字取整。向下取整。

如果第一个字符是数字会解析直到遇到非数字结束,如果第一个字符不是数字或者符号(如:+、-)就返回 NaN。

带自动净化功能;带自动截断小数功能,且取整,不四舍五入。

radix: 基数,也就是进制.介于 2-36 之间,超出都是返回 NaN.radix 不写默认是十进制.如果传入字符串是 0x 开头,默认是 16 进制.如果是 0 开头,默认 8 进制.任何数的 0 进制都是它本身.高于进制位能表示的最大值数返回 NaN.比如说 2 的一进制返回 NaN,因为一进制只能用 1 表示,2 作为一进制来表示明显不对.

几次方

1
2
Math.pow(x, y); //返回 x 的 y 次幂的值。
Math.pow(2, 2); //4

平方根

1
2
Math.sqrt(4); // 2
Math.sqrt(-4); //NaN

random

1
2
Math.random();
//返回0-1之间的伪随机数.0≦x<1

Date

1
2
3
Date.now(); //获取1970年至今的毫秒数
Date.parse(); //解析日期字符串,返回距离1970年的毫秒数
Date.parse("2011-01-11"); //13234436600000
1
2
3
4
5
6
7
new Date(); //使用Date构造函数创建一个Date实例,
//不传时间就返回当前时间的字符串

var str = "2019-01-11";
new Date(str);
//Fri Jan 11 2019 08:00:00 GMT+0800 (香港标准时间)
//获取的是东八区8点的时间,因为格林尼治时间伦敦是0点

get 方法

1
2
3
4
5
6
7
8
getDate(); //日
getDay(); //星期几,周日是0,周一是1
getFullyear(); //返回四位的年份
getMouth(); //返回月份,一月是0,腊月是11
getHours(); //小时(0-23)
getMinutes(); // 返回分钟
getSenconds(); // 秒
getMilliseconds(); // 毫秒(0-999)

set 方法类似于 get 方法,是设置时间

时间格式化过滤器

Vue 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{{date | dateFormat }}

<script>
filters: {
//时间格式化
dateFormat: function(date) {
//date是传入的时间字符串
let t = new Date(date);
return (
t.getFullYear() +
"-" +
(t.getMonth() + 1) +
"-" +
t.getDate() +
" " +
t.getHours() +
":" +
t.getMinutes() +
":" +
t.getSeconds()
);
}
},
</script>

异步和回调

setTimeout 和 setInterval

区别: setInterval 指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行.

清除定时器: clearTimeout()和 clearInterval()

运行机制

setTimeout 和 setInterval 的运行机制是,将指定的代码移出本次执行,等到下一轮 Event Loop,再检查是否到了指定时间.如果到了,就执行对应代码,不到,就等到下一轮重新判断.这也就意味着,setTimeout 指定的代码,必须等到本次执行的所有代码都执行完,才会执行.

单线程模型

异步的任务放在任务队列(callback queue)中,setTimeout 也是.必须等放在栈(stack)中的同步代码执行完,才会执行任务队列中的.

event loop(事件循环)

经典案例:

1
2
3
4
5
for(var i=0; i<5; i++){
setTimeout(fucntion timer(){
comsole.log(i)
},1000)
}

我的理解:

开始循环遍历.(同步代码)

i=0.创建一个 console.log(i).放到队列里,并不执行.

i=1.创建一个 console.log(i).放到队列里,并不执行,

直到 i=5.依次创建了 5 个 console.log(i)

因为 setTimeout 是异步,所以必须等同步执行完才开始执行异步.

同步代码 i 循环到 5,输出,有 5 个 console.log(i)在队列里嗷嗷待哺

,把 i=5 赋值进去.得到 5 个 5.

函数节流

在时间内,如果执行,就重新开始

原理:当达到了一定的时间间隔就会执行一次;可以理解为是缩减执行频率

函数节流会用在比 input, keyup 更频繁触发的事件中,如 resize, touchmove, mousemove, scroll。throttle 会强制函数以固定的速率执行。因此这个方法比较适合应用于动画相关的场景。

常用场景:

  1. 搜索框搜索输入。只需用户最后一次输入完,再发送请求
  2. 手机号、邮箱验证输入检测
  3. 窗口大小 Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

简单版本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function throttle(fn, ms) {
let timer; //命名一个定时器
return function () {
if (timer) {
//如果有定时器就退出
return;
}
timer = setTimeout(fn, ms);
};
}
function fn() {
console.log("hello");
}
var fn2 = throttle(fn, 1000);

时间戳版本:

1
2
3
4
5
6
7
8
9
10
const throttle = (fn, wait) => {
let last = 0;
return () => {
const current_time = +new Date();
if (current_time - last > wait) {
fn.apply(this, arguments);
last = +new Date();
}
};
};

函数防抖

原理:将若干函数调用合成为一次,并在给定时间过去之后,或者连续事件完全触发完成之后,调用一次(仅仅只会调用一次)

适合场景:

  1. 滚动加载,加载更多或滚到底部监听
  2. 谷歌搜索框,搜索联想功能
  3. 高频点击提交,表单重复提交
1
2
3
4
5
6
7
8
9
10
11
12
function debounce(fn, ms) {
let timer = null;
return () => {
clearTimeout(timer);
timer = setTimerout(fn, ms);
};
}
//实际上上面这就可以了
function vlog() {
console.log(1);
}
window.onscroll = debounce(vlog, 500);

带有立即执行选项的防抖函数

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
40
41
42
43
44
45
46
47
48
49
// 这个是用来获取当前时间戳的
function now() {
return +new Date();
}
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行
*
* @param {function} func 回调函数
* @param {number} wait 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
function debounce(func, wait = 50, immediate = true) {
let timer, context, args;

// 延迟执行函数
const later = () =>
setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null;
// 延迟执行的情况下,函数会在延迟函数中执行
// 使用到之前缓存的参数和上下文
if (!immediate) {
func.apply(context, args);
context = args = null;
}
}, wait);

// 这里返回的函数是每次实际调用的函数
return function (...params) {
// 如果没有创建延迟执行函数(later),就创建一个
if (!timer) {
timer = later();
// 如果是立即执行,调用函数
// 否则缓存参数和调用上下文
if (immediate) {
func.apply(this, params);
} else {
context = this;
args = params;
}
// 如果已有延迟执行函数(later),调用的时候清除原来的并重新设定一个
// 这样做延迟函数会重新计时
} else {
clearTimeout(timer);
timer = later();
}
};
}

对于按钮防点击来说的实现:

如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。

对于延时执行函数来说的实现:

清除定时器 ID,如果是延迟调用就调用函数

节流和防抖的区别

他们的区别在于防抖只会在连续的事件周期结束时执行一次,而节流会在事件周期内按间隔时间有规律的执行多次。

我的理解:

函数节流:

只要又执行函数,就退出.必须等上一次定时器任务执行完.这一段时间只执行一次.

函数防抖:

只要又执行函数,就清除定时器,重新设置定时器.只要重复就重新开始,最后肯定有一次.

DOM 元素创建和选取

DOM 是文档对象模型,是 html 的编程接口

readyState 加载状态

1
2
3
4
5
6
7
8
9
10
11
document.location;
//location属性返回一个只读对象,提供当前文档的url信息
document.location.href; //获取url
document.location.protocol; //获取"http:"
document.location.hostname; // "www.example.com"
document.location.port; // "4097"
document.location.pathname; // "/path/a.html"
document.location.assign("http://www.Google.com");
//跳转到指定网址
document.location.reload(true); //优先从服务器重新加载
document.location.reload(false); //优先从本地缓存加载
1
2
3
document.open(); //新建文档可以使用write写入
document.close(); //关闭文档
document.write(); //写入文档,会清除之前的文档流

Element表示 html 元素

  1. element 的属性:
  2. nodeName: 元素标签名
  3. nodeType:元素类型
  4. className:类名
  5. id:元素 id
  6. children:子元素列表
  7. childNodes: 子元素列表(NodeList)
  8. firstChild: 第一个子元素
  9. lastChild:最后一个
  10. nextSibling: 下一个兄弟元素
  11. previousSibling: 上一个兄弟元素
  12. parentNode.parentElement: 父元素

查询元素

1
2
3
4
5
6
7
8
9
10
11
12
13
document.getElementById("target");

document.getElementsByClassName("box");
//上述为ES3写法
//下面是ES5写法
document.querySelector(".box");
document.querySelector("#tatget");
//多个元素只会选择第一个

//选择多个
document.querySelectorAll("div");
//浏览器控制台测试时可以用$替代document.querySelector
//用$$替代document.querySelectorAll

创建元素

1
2
3
4
5
6
7
document.createElement("div");
//生成文本节点
document.createTextNode("Hello");
//生成DocumentFragment对象
document.createDocumentFragment();
//存在于内存中的DOM片段,不属于当前文档。
//对它的任何改动,都不会引发网页的重新渲染

修改元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
在元素末尾添加元素;
appendChild();
newDiv.appendChild(newContent);

在元素之前插入元素;
insertBefore();

替换元素;
replaceChild(newElement, oldElement);

删除元素;
parentNode.removeChild(childNode);

clone元素;
node.cloneNode(true);
//方法有一个布尔值,传入true时会深拷贝,
//复制元素及其子元素,false只会复制元素本身

DOM 元素属性

1
2
3
4
5
6
7
8
9
10
11
12
获取属性
node.getAttribute("id")

生成新的属性对象节点
attribute = document.createAttribute(name)

一般用 setAttribute()设置元素属性
var node = document.getElementById("id")
node.setAttribute("id", "newVal")

删除属性节点
node.removeAttribute("id")
  • innerText可写属性,返回元素内包含的文本内容,在多层次的时候按照元素由浅到深的顺序拼接内容
1
2
3
4
5
6
<div>
<p>
123
<span>456</span>
</p>
</div>

外层 div 的 innerText 返回内容是  "123456"

  • innerHTML返回元素 html 结构,在写入的时候也会自动构建 DOM

样式

修改样式

1
document.querySelector("p").style.color = "red";

获取样式 getComputedStyle

1
2
3
4
var node = document.querySelector("p");
var color = window.getComputedStyle(node).color;
console.log(color);
//获取计算后的样式,只读。全局属性

class 操作的增删改查

1
2
3
4
5
6
var nodeBox = document.querySelector(".box");
console.log(nodeBox.classList);
nodeBox.classList.add("active"); //新增class
nodeBox.classList.remove("active"); //删除
nodeBox.classList.toggle("active"); //新增删除切换
node.classList.contains("active"); //判断是否拥有class

页面宽高

1
2
3
4
5
6
7
element.clientHeight; //包括padding,相当于IE盒模型,content-box
element.offsetHeight; //包括border,相当于标准盒模型,border-box
element.scrollHeight; //滚动区域的总高度(元素的起始位置的底边到停止位置的顶边),包括元素的padding,但不包括元素的border和margin
element.scrollTop; //滚动的高度
element.offsetTop; //返回当前元素相对于其 offsetParent 元素的顶部内边距的距离。
window.innerHeight; //视口的高度,不包括标题栏等,包括水平滚动条
window.outerHeight; //整个浏览器窗口的高度,包括标题栏等所有浏览器内容

如何判断一个元素是否出现在窗口视野中

滚动的距离 + 窗口的高度 = 目标元素到顶部的距离

1
2
3
`window.scrollTop + window.innerHeight = element.offsetTop`;
//方法二
$("element").offset().top <= $(window).height() + $(window).scrollTop();

如何判断页面滚动到底部

1
2
3
4
5
//窗口滚动的距离 + 窗口的高度 = 整个页面的高度
$(window).scrollTop() + $(window).height() = $('body').height()
//方法二
//元素滚动的距离 + 元素的高度 = 元素的滚动区域的高度
element.scrollTop + element.clientHeight = element.scrollHeight

事件

DOM 事件流: 捕获阶段,目标阶段,冒泡阶段

事件绑定

1
2
3
4
5
6
7
8
<input id="btnClick" type="button" value="Click here">

<script type="text/javascript">
var btnClick = document.getElementById("btnClick")
btnClick.onclick = function showMessage(){
alert(this.id)
}
</script>

点击事件是异步的.

会存在覆盖

DOM2 事件处理(升级版)

  • addEventListener //绑定事件
  • removeEventListener //解绑事件

所有 DOM 节点都包含这两个方法,且都接受三个函数:

  1. 事件类型
  2. 事件处理方法
  3. 布尔参数,true表示在捕获阶段调用,false表示在事件冒泡阶段处理,默认是冒泡阶段
1
2
3
4
5
6
7
btnClick.addEventListener(
"click",
function () {
alert(this.id);
},
false
);

不存在覆盖,可以写多个方法

属性方法

1
2
3
preventDefault(); //取消默认事件行为
stopPropagation(); //取消事件进一步捕获或冒泡
target(); //事件的目标元素

事件代理

事件委托(代理)就是利用冒泡的原理,把事件加到父元素或祖先元素上,触发执行效果。

使用事件代理来实现它,监听的元素应该是这些元素的父元素,当我点击父元素内的元素时,父元素都会得到响应,并分发相应的事件。  e.target就是点击的元素。

1
2
3
4
5
6
7
$(".container").onclick = function (e) {
console.log(this);
console.log(e.target);
if (e.target.classList.contains("box")) {
console.log(e.target.innerText);
}
};

常见事件及自定义事件

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
40
41
42
43
44
45
46
47
48
49
50
鼠标事件;
click;
dblclick; //双击左键
mouseover; //鼠标放上
mouseout; //鼠标移出
mouseenter; //类似over,区别不计算子元素
mouseleave; //类似out,区别不计算子元素
mousedown; //鼠标按下
mouseup; //鼠标松开

触摸事件;
touchstart;
touchend;
touchmove;

键盘事件;
keyup; //按键后松开触发
keydown; //按键即会触发
keypress; //按下松开

页面相关事件;
onload; //加载完成时触发
onmove; //浏览器窗口被移动
scroll; //滚动条滚动
resize; //窗口大小变化

表单相关;
focus; //获取焦点
blur; //失去焦点
change; //失去焦点且内容发生改变
reset; //reset属性被激活
submit; //提交,一般是表格提交
input; //在input元素内容修改后立即触发
onload; //页面所有资源加载完成
DOMContentLoaded; //dom结构解析完成

编辑事件;
beforecopy; //复制之前
beforecut; //剪切之前
beforepaste; //粘贴之前
beforeeditfocus; //将要进去编辑状态
contextmenu; //按右键出现菜单或者键盘触发页面菜单
losecapture; //失去鼠标移动所形成的选择焦点
select; //被选择时

拖动事件;
drag; //某个对象被拖动
dragdrop; //外部对象被拖到当前窗口
dragend; //拖动结束
dragenter; //被拖动对象进入其容器范围

js 拖动 div

  1. 使用mouse实现
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
40
41
42
<head>
<meta charset="UTF-8" />
<title> mouse实现div拖动 </title>
<style type="text/css">
#div {
position: absolute;
left: 100px;
top: 100px;
width: 200px;
height: 200px;
background-color: #f60;
}
</style>
</head>
<body>
<div id="drag"></div>
<script type="text/javascript">
var drag = document.getElementById("drag");
var dragFlag = false;
var x,y;

drag.onmousedown = function (e) {
e = e || window.event;
x = e.clientX - drag.offsetLeft;
y = e.clientY - drag.offsetTop;
dragFlag = true;
};

document.onmousemove = function (e) {
if (dragFlag) {
e = e || window.event;
drag.style.left = e.clientX - x + "px";
drag.style.top = e.clientY - y + "px";
}
};

document.onmouseup = function (e) {
dragFlag = false;
};

</script>
</body>
  1. drag 实现
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
<head>
<style>
.divblok {
background-color:#ccc;
width:30px;
height:30px;
position:absolute;
left:20px;
top:20px;
}
</style>
</head>
<body>
<div id="dragdiv" draggable="true" class="divblok">我要移动 </div>
<script>
var dragdiv = document.querySelector('#dragdiv');
var x, y; //记录到点击时鼠标到移动框左边和上边的距离

dragdiv.addEventListener('dragstart', function (e) {
e.dataTransfer.effectAllowed = "move"; //移动效果
e.dataTransfer.setData("text", ''); //附加数据, 没有这一项,firefox中无法移动
x = e.offsetX || e.layerX;
y = e.offsetY || e.layerY;
return true;
}, false);

document.addEventListener('dragover', function (e) {//取消冒泡 ,不取消则不能触发 drop事件
e.preventDefault()|| e.stopPropagation();
}, false);

document.addEventListener('drop', function (e) {
dragdiv.style.left = (e.pageX - x) + 'px';
dragdiv.style.top = (e.pageY - y) + 'px';
e.preventDefault() || e.stopPropagation(); //不取消,firefox中会触发网页跳转到查找setData中的内容
}, false);
</script>
</body>

自定义事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var EventCenter = {
on: function (type, handler) {
document.addEventListener(type, handler);
},
fire: function (type, data) {
return document.dispatchEvent(
new CustomEvent(type, {
detail: data,
})
);
},
};
EventCenter.on("hello", function (e) {
console.log(e.detail);
});
EventCenter.fire("hello", "你好");
//一个监听函数,一个处理函数

JS 动画

requestAnimationFrame

可以在一定时间内自动执行

1
2
3
4
5
6
7
8
9
function move() {
if (moveOffset < offsetX) {
ball.style.left = parseInt(getComputedStyle(ball).left) + step + "px";
moveOffset += step;
requestAnimationFrame(move);
}
}

move();

BOM

bom 指浏览器对象模型,核心是 window 对象。是浏览器的实例

window.innerHeight属性,window.innerWidth属性.这两个属性返回网页的 CSS 布局占据的浏览器窗口的高度和宽度,单位为像素。很显然,当用户放大网页的时候(比如将网页从 100%的大小放大为 200%),这两个属性会变小。

注意,这两个属性值包括滚动条的高度和宽度。

  1. scrollX:滚动条横向偏移
  2. scrollY:滚动条纵向偏移

这两个值随着滚动位置变化而变化

window.frames返回一个类似数组的对象,成员为页面内的所有框架,包括 frame 元素和 iframe 元素。

需要注意的是,window.frames 的每个成员对应的是框架内的窗口(即框架的 window 对象),获取每个框架的 DOM 树,需要使用window.frames[0].document

1
2
var iframe = window.getElementsByTagName("iframe")[0];
var iframe_title = iframe.contentWindow.title;

上面代码用于获取框架页面的标题。

iframe 元素遵守同源政策,只有当父页面与框架页面来自同一个域名,两者之间才可以用脚本通信,否则只有使用 window.postMessage 方法。

在 iframe 框架内部,使用 window.parent 指向父页面。

指向一个包含浏览器相关信息的对象.

window.getComputedStyle

getComputedStyle 是一个可以获取当前元素所有最终使用的 CSS 属性值

1
var style = window.getComputedStyle("元素", "伪类")

URL 的编码/解码方法

JavaScript 提供四个 URL 的编码/解码方法。

  1. decodeURI()
  2. decodeURIComponent()
  3. encodeURI()
  4. encodeURIComponent()

区别

encodeURI 方法不会对下列字符编码

1
2
3
1. ASCII字母
2. 数字
3. ~!@#$&*()=:/,;?+'

encodeURIComponent 方法不会对下列字符编码

1
2
3
1. ASCII字母
2. 数字
3. ~!*()'

alert(),prompt(),confirm()

alert()、prompt()、confirm()都是浏览器用来与用户互动的方法。它们会弹出不同的对话框,要求用户做出回应。

需要注意的是,alert()、prompt()、confirm()这三个方法弹出的对话框,都是浏览器统一规定的式样,是无法定制的。