山外小楼

前端攻略

0%

问题

许多网站的访问在网址后面有时会带一个‘/’,有时没有,但经常带不带都访问的都是同一个网页;最近在用django的时候发现这个问题;现记录下来。

表现

在django的框架下,访问路由地址的时候,如果你的路由地址配置为如下这种

1
url(r'^user/$', 'get_user_info')

框架默认会将不带斜杠的路由重定向到带斜杠的路由下,这是访问***.com/user***.com/user/都可以;

如果配置为

1
url(r'^user$', 'get_user_info')

这在访问***.com/user时没有问题,访问***.com/user/时就会报404错误。

注意的是,这种重定向会被浏览器记住,当你改变配置为不带斜杠的时候,继续访问浏览器会被重定向到带斜杠的地址,而发生404错误;

原因

django的配置文件settings.py中,默认的设置(该配置未显式写出)为Ture;

1
APPEND_SLASH = True

指示不带斜杠的情况下会重定向到带斜杠的地址;如果设置为False,就不会重定向;

引言

针对前端安全,主要有XSS跨站脚本攻击,CSRF跨站伪造请求,SQL注入等,今天主要讨论下关于前两种

XSS跨站脚本攻击

更多具体详细的解释可以查看XSS(跨站脚本攻击)的最全总结

XSS攻击通常被分为两类:存储型和反射型。还有第三类,和基于DOM的xss;下面我们不说定义直接看看怎么操作

  1. 反射型

一般有搜索框的网站,在搜索东西的时候会在结果页呈现你搜索的字符;

比如你搜索笔记本电脑,网站会提示你搜索“笔记本电脑”的结果如下,这说明用户的输入在页面上有了呈现;那么我们输入一段恶意脚本,也许也会原样输出执行,比如我们输入

1
<script>alert(1)</script>

恶意代码就是你可以放一段脚本,比较加入一个js文件请求,在js代码里来获取cookie等用户信息,并发送给自己;但用户并不会自己输入恶意脚本,那么就通过邮件等方式,让用户点击搜索结果连接,以达到脚本注入的目的;

当然基本上没什么网站会有这么明显的漏洞,这里只是演示说如何实现反射型攻击。

  1. 存储型

这种一般常出现在用户可输入,然后在网站动态展示,大家都能看到的网站,如微博,社区,评论等地方;常见的做法就是发一段脚本到输入区,网站会记录下来并动态的渲染在页面上,只要访问的用户都能看到;由于这种情形注入的脚本被写到了服务器数据库中,所以叫存储型攻击;

  1. 基于DOM

这种攻击的前提是易受攻击的网站有一个HTML页面采用不安全的方式从document.location 或document.URL 或 document.referrer获取数据(或者任何其他攻击者可以修改的对象),也就是说恶意代码直接来自这些可任意被修改的对象;详细可查看基于 DOM 的第三类跨站脚本 XSS

如上,一般只要这个网站某个页面将用户的输入包含在它生成的动态输出页面中并且未经验证或编码转义,这个缺陷就存在。

  1. 防范方法

    1. 所有前端的页面渲染,尽量使用ajax异步进行,从后台获取要显示的数据。
    2. 前端提交过来的数据,在后台入口处统统对HTML中的关键字进行html编码转义。

CSRF

更多具体详细的解释可以查看邪恶的CSRF – superfish

CSRF叫跨域伪造请求,其实就是模拟用户操作,就是在A网站登录的情况下,B网站可以发起A网站发起的请求;要达成这个目的就需要解决跨域的问题;

传统的ajax都有跨域的限制,但如发起get请求就很方便,以打开url的方式,jsonp的方式,请求三方资源的方式,都可以达成目的;

而采用原生表单的提交方式,可以发起post跨域请求;

该篇文章很多段落来自单点登录原理与简单实现,大家可以先拜读这篇文章,这篇文章讲述十分详细,我这里只是插了一些自己的疑虑,而且可能还说的不是很清楚,权当自己做个笔记了

单系统登录机制

  1. http无状态协议

http作为通信协议,是无状态的,浏览器的每一次请求,服务器会独立处理,不与之前或之后的请求产生关联。

但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;要鉴别浏览器请求,必须清楚浏览器请求状态。既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制。

  1. 会话机制

浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,服务器取得请求中的会话id就知道是不是同一个用户了。

这里有个session和cookie的区别;其实session算是cookie的变种;因为本身的机制形式并没有变,依然是需要浏览器有一个信息串和服务器得会话对象所对应,只不过早先得cookie是在浏览器端保存得用户的信息,服务器通过这些信息来识别用户,而session方式是把信息保存在了服务器端,然后生成一个session的id字符串存在了cookie,相对之前的做法安全一些;但本质未发生变化。

当然,浏览器但端的信息其实不是必须存在cookie里,也可以放在localstotage或者其他可存的地方,这样防止从cookie得到敏感信息,但这样就需要前端做一个手工处理,将这个id发送给服务器端;目前大多的系统还是主要以cookie来存储的,一是读写cookie浏览器可以自动完成,二是在单点登录系统时可以帮其他子应用自动登录。

  1. 登录状态

有了会话机制,登录状态就好明白了,我们假设浏览器第一次请求服务器需要输入用户名与密码验证身份,服务器拿到用户名密码去数据库比对,正确的话说明当前持有这个会话的用户是合法用户,应该将这个会话标记为“已授权”或者“已登录”等等之类的状态。

个人理解,http是无状态的,那么每个受限请求都是需要验证的,那么前端传来的id只要可以对应后端的session,那么就是合法的,所以不太清楚后端是否还要做一个会话标记的操作;

多系统的复杂性

web系统由单系统发展成多系统组成的应用群,复杂性应该由系统内部承担,而不是用户。无论web系统内部多么复杂,对用户而言,都是一个统一的整体,也就是说,用户访问web系统的整个应用群与访问单个系统一样,登录/注销只要一次就够了。

单系统登录解决方案的核心是cookie,cookie携带会话id在浏览器与服务器之间维护会话状态。但cookie是有限制的,这个限制就是cookie的域(通常对应网站的域名),浏览器发送http请求时会自动携带与该域匹配的cookie,而不是所有cookie。也就是说A应用登录后,打开B应用,B是拿不到A的cookie的。

这时我们就将web应用群中所有子系统的域名统一在一个顶级域名下,在顶级域名下设置的cookie,其子域名是可以访问其cookie的。

这里有个cookie设置的规则(有点儿像全局作用域和子作用域):
设置cookie只可以向上设置,可设置本级域名,或者向上的域名;比如顶级域名只能设置domain为顶级域名,不能设置为二级域名或者三级域名等等,否则cookie无法生成;二级域名可以设置本域及顶级域;
获取cookie只可以向上获取,可获取本级域名,或者向上的域名;比如顶级域名只能获取到domain设置为顶级域名的cookie,domain设置为其他子级域名的无法获取;二级域名能读取设置了domain为顶级域名或者自身的cookie,不能读取其他二级域名domain的cookie。
总的来说,顶级域名设置的cookie可以共享【需要指定domain主域名的host】给二级域名,也可以自己私有【不指定domain】

  • 这里云平台系统采用的就是这种方式,sessionid由统一认证系统生成,并写到顶级域名下面,其余子系统通过访问cookie获取该id与统一认证系统进行权限验证;不过这里是子系统通过统一认证系统生成的token然后在服务端去统一认证系统换取sessionid,由子系统写到cookie里的顶级域名下的,不太清楚这一步是什么意思;这样,各个子系统的共享统一生成的sessionid,并且每个受限请求都去找认证系统授权;这种方式也很方便的可以做到统一登出,只要使统一认证系统的session失效就可以了。

然而,可行并不代表好,共享cookie的方式存在众多局限。首先,应用群域名得统一;其次,应用群各系统使用的技术(至少是web服务器)要相同,不然cookie的key值(tomcat为JSESSIONID)不同,无法维持会话,共享cookie的方式是无法实现跨语言技术平台登录的,比如java、php、.net系统之间;第三,cookie本身不安全。

因此,我们需要一种全新的登录方式来实现多系统应用群的登录,这就是单点登录。

单点登录

这里说的单点登录,是指有一个统一认证中心,可实现一处登录,全都登录,一处登出,全部登出,并且不局限与同一个顶级域名下。

单点登录的简单流程如下图:

单点登录流程

单点登出流程

其中有两个主要注意的地方,就是在认证中心登录的时候,会帮其他子系统都登录,登出的时候会帮其他子系统都登出;另外子系统的sessionid是各自维护还是统一有认证系统维护;

  1. 统一登录登出机制

在统一认证中心登录后,可以以跨域访问的形式,将统一的生成的token,逐个发送给各个子系统,子系统收到后向认证中心认证token,认证通过后可以给自己系统下写入cookie,这样,在访问其他子系统的时候,请求会带上之前写的cookie,从而打开子系统即为登录状态;

在子系统登出时会跳转到登录页面,此时可以以跨域访问的形式,逐个访问各个子系统请求,这些请求会携带各自域名下的sessionid,子系统就会注销该id,完成各个子系统的登出;

  1. sessinid维护方式

如果各自维护sessionid,登录的时候不通知子系统的话,打开其他子系统的时候会有一个重定向到认证中心获取状态的过程;通知了的话就无需重定向了;在登出的时候认证中心必须去通知各个子系统登出,不然子系统无法知道已登出;

如果认证系统统一维护sessionid的话,登录不通知也会去重定向;登出就不必通知了。

  • 如金融的sso认证系统便没有去通知其他子系统登录状态,在访问子系统的时候会重定向到认证地址,认证地址根据cookie信息,确认已登录后,将token带在请求上,重定向回子系统,子系统拿到token后去认证中心通过后再写sessionid,这样会有一个重定向url变更的过程;而登出的时候,由于并未通知其他子系统,所以登出无法做到统一登出;不过,此处既然没有做统一登出,那么在登录的时候也不应该去通知子系统去登录;这样这个系统其实算是个统一认证中心,不算严格意义的单点登录。
  1. 通知其他子系统登录登出的形式

由于域名不同,通知其他子系统只能以跨域的形式去访问各自的服务器;可以以jsonp的形式,也可以是cors的形式,或者其他跨域方式,但需要将统一认证生成的token信息手动加到请求中,因为跨域请求不会携带本域的cookie信息。

问题引出

由于django本身的权限管理是基于cookie来认证的,而整个过程并不需要前端做什么,直接后端完成;但跨域的时候,产生几个问题;

  1. django如何跨域?
  2. django跨域后可以正常的写入cookie吗?

其实,跨域实现的方法有很多种,可以查看前端常见跨域解决方案(全),在此我们使用cors跨域的方式

cors跨域概述

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)

相关内容可以参考跨域CORS原理及调用具体示例

引起我们注意的一点就是同为post请求,当Content-Type的值为application/json时,就变成了非简单请求,基础表现就是浏览器会先发一个”预检”请求

django跨域设置

  1. 执行安装
1
2
# https://github.com/ottoyiu/django-cors-headers/
pip install django-cors-headers
  1. 配置settings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
INSTALLED_APPS = (
...
'corsheaders',
...
)
...
MIDDLEWARE = [ # Or MIDDLEWARE_CLASSES on Django < 1.10
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
...
# cookies will be allowed to be included in cross-site HTTP requests
CORS_ALLOW_CREDENTIALS = True
# If True, the whitelist will not be used and all origins will be accepted
CORS_ORIGIN_ALLOW_ALL = False
# A list of origin hostnames that are authorized to make cross-site HTTP requests
CORS_ORIGIN_WHITELIST = (
'localhost:8888'
)

显式设置cookie

1
2
3
4
5
6
7
8
9
10
11
# view.py
def test(request):
if request.method == 'POST':
data = {
'code': 0,
'msg': 'this is a test',
'data': 'POST'
}
rep = HttpResponse(json.dumps(data, cls=CJsonEncoder), content_type="application/json")
rep.set_cookie('key', 'value')
return rep

前端设置

前端默认不允许跨域携带cookies,需进行显式的设置;不同的库可能有一些细微的差别;

需要注意的是,以原生表单的形式,发起post请求,是可以携带cookie自动跨域的,无需其他设置;form表单可以跨域一个是历史原因要保持兼容性,一个是form表单会刷新页面不会把结果返回给js,所以相对安全,但其实请求已经发送出去了,你只是拿不到响应而已;这个问题会引起CSRF攻击;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 原生js(亲测可用,其余未作测试)
var xhr = new XMLHttpRequest();
xhr.open("POST", "http://localhost:8000/api/test/", true);
xhr.withCredentials = true; //支持跨域发送cookies
xhr.send();

// jQuery
$.ajax({
type: "POST",
url: "http://localhost:8000/api/test/",
dataType: 'jsonp',
xhrFields: {withCredentials: true},
crossDomain: true,
})

// axios(vue常用的方案)
axios.create({
timeout: 5000,
withCredentials: true // 允许携带cookie
})

突然来的疑问

写到此时,我所作的测试都是在本地测的,服务器端在localhost:8000,前端在localhost:8888;而跨域操作所写的cookie均在localhost之下,这样就有一个问题,跨域读写的cookie到底是哪里?是发起请求页的域还是请求地址的域?

解答疑问

我们得到的答案是cors跨域读写的cookie都是请求地址的cookie;比如我们请求第三方cdn,并不会携带本站的cookie,发送跨域请求也一样,只会携带请求指向地址的域的cookie。(这也是跨站伪造请求csrf的原理)

服务器如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

如果非要跨域写cookie呢?有什么办法吗?

跨域写本域cookie

查到的方法如下:
可以通过ngix反向代理跨域配置:proxy_cookie_domain b.com a.com;
也可以通过中间件http-proxy-middleware代理跨域时,配置加上cookieDomainRewrite: ‘a.com’参数,都是用来修改转发过来的cookie中域名为当前a.com域,所以实现了a.com下cookie写入。

前言

HTTP/1.1 协议规定的 HTTP 请求方法有 OPTIONS、GET、HEAD、POST、PUT、DELETE、TRACE、CONNECT 这几种。其中 POST 一般用来向服务端提交数据,本文主要讨论 POST 提交数据的几种方式。
我们知道,HTTP 协议是以 ASCII 码传输,建立在 TCP/IP 协议之上的应用层规范。规范把 HTTP 请求分为三个部分:状态行、请求头、消息主体。
协议规定 POST 提交的数据必须放在消息主体(entity-body)中,但协议并没有规定数据必须使用什么编码方式。实际上,开发者完全可以自己决定消息主体的格式,只要最后发送的 HTTP 请求满足上面的格式就可以。
但是,数据发送出去,还要服务端解析成功才有意义。一般服务端语言如 php、python 等,以及它们的 framework,都内置了自动解析常见数据格式的功能。服务端通常是根据请求头(headers)中的 Content-Type 字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。所以说到 POST 提交数据方案,包含了 Content-Type 和消息主体编码方式两部分。下面就正式开始介绍它们。

个人理解:get请求其实就是将参数以key=value&key=value的形式拼在url发往服务器,所以get请求没有那么多复杂的说法;而post请求规定提交的数据必须放在消息主体(entity-body)中,但没有对形式的规定,所以就有了各种各样的方式;

引出问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var ajax = new XMLHttpRequest();
var data1 = '{"arr":[1,2,3,4,5],"test":"name"}'
var data2 = 'name=jack&age=998'
// 使用post请求
ajax.open('post', 'http://localhost:8000/api/test/');

// 如果 使用post发送数据 必须 设置 如下内容
// 修改了 发送给 服务器的 请求报文的 内容
// 如果需要像 HTML 表单那样 POST 数据,请使用 setRequestHeader() 来添加 HTTP 头。
// 然后在 send() 方法中规定您希望发送的数据:
ajax.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
// 发送
// post请求 发送的数据 写在 send方法中
// 格式 name=jack&age=18 字符串的格式;
// 不能直接发送一个对象,否则后端会变成[object Object]
ajax.send(data2);

// 注册事件
ajax.onreadystatechange = function () {
if (ajax.readyState == 4 && ajax.status == 200) {
console.log(ajax.responseText);
}
}

需要注意的是,ajax.send()的内容可以是data1的格式(JSON.stringfy()得来),也可以是data2的格式(qs.stringfy()得来),两种的方式都可以;

另外ajax.send()不能直接发送对象,后端接受会变成[object Object],而对于axios库,直接发送对象,它会自动转成json字符串;

在后端为django的代码中测试

1
2
3
4
5
6
7
8
9
10
def test(request):
if request.method == 'POST':
print request.POST.get('arr')
print request.body
data = {
'code': 0,
'msg': 'this is a test',
'data': 'POST'
}
return HttpResponse(json.dumps(data, cls=CJsonEncoder), content_type="application/json")

如何是data1的格式就是json字符串的话,request.POST是无法获取到东西的,data2格式通过request.POST是可以获取到的,但前提是请求的Content-type必须设置为application/x-www-form-urlencoded,否则都获取不到结果,只能从request.body中获得原始数据;
之所以这样是因为django框架在处理响应时,只对Content-typeapplication/x-www-form-urlencoded的情况做了处理,(具体可以查看django的源码中class HttpRequest(object)对post的处理部分)
django之所以这么做,是因为什么呢?下面先说一下关于post的几种请求方式

application/x-www-form-urlencoded

这是post的默认请求方式,在post请求不设置Content-type时的默认值;

1
2
3
4
5
6
<form action="http://localhost:8000/api/test/" method="post">
First name:<br>
<input type="text" name="firstname" value="Mickey"><br> Last name:<br>
<input type="text" name="lastname" value="Mouse"><br><br>
<input type="submit" value="提交">
</form>

这也是原始提交表单的方式,在表单提交的方式里,浏览器会把表单里的数据,以key=value&key=value的形式发送给服务器;个人理解,由于这种最初的提交方式,决定了application/x-www-form-urlencoded下对数据的处理;
所以,在ajax请求的年代,其实传哪种(data1和data2)格式都可以,不同的是,服务器是以Content-type的类型来决定如何处理数据的;

这种post的类型的时候可以向服务器发送数据,而数据的格式应为键值对key=value&key=value,js对象可以通过qs.stringfy()来序列化得到;不过,qs在序列化数组的时候默认会序列化为arr[]=value&arr[]=value的形式;这时需要配置下qs,指定数组的编码格式;否则后端获取不到该数组;

django接收数组的时候用request.POST.getlist();如果前端序列化带了[],后端也需要带,或者要前端序列化的时候去掉[]

指定数组的编码格式和对应的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let params = [1, 2, 3];

// indices(默认)
qs.stringify({a: params}, {
arrayFormat: 'indices'
})
// 结果是
'a[0]=1&a[1]=2&a[2]=3'

// brackets
qs.stringify({a: params}, {
arrayFormat: 'brackets'
})
// 结果是
'a[]=1&a[]=2&a[]=3'

// repeat
qs.stringify({a: params}, {
arrayFormat: 'repeat'
})
// 结果是
'a=1&a=2&a=3'

application/json

这种方式就是告诉服务器,传输的数据为json字符串,所以对应的数格式应为json字符串,js对象可以通过JSON.stringfy()来序列化得到;这是一种略新的格式,可能早期的ie等浏览器不支持

如果发送对象比较复杂比如[{"id":"001","name":"小明"},{"id":"002","name":"小军"}];这时候还是简单的用json字符串为好,表单提交的方式可能会造成后端解析困难的情况,另外也很难看懂结果;

multipart/form-data

这种类型主要用来上传文件(可上传多个),也可以发送键值对;(该内容后续展开)

text/plain (text/json/xml/html)

数据以纯文本形式(text/json/xml/html)进行编码,其中不含任何控件或格式字符。postman软件里标的是RAW,application/json包含在RAW里;

binary

暂没用过,只在postman中看到过,相当于Content-Type:application/octet-stream,从字面意思得知,只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件。

js变量的类型

我们知道js变量有两种类型

  • 基本类型值,指简单的数据段,对于undefined、null、boolean、number、string这5种简单数据类型可以直接操作保存在变量中的实际值,也就是按值访问。
  • 引用类型值,指那些可能由多个值构成的对象,只能操作对象的引用而不是实际的对象,所以要得到引用类型这种值只能按引用访问。

变量的存放方式

有如下代码段(赋值基本类型)

1
2
var a=1;
var b=1;

上述代码定义了两个变量,定义a=1在栈内存开辟一块空间存放1这个值,同理定义b=1也开辟一块内存存放1这个值;
当修改变量的值的时候,比如重新赋值a=3,那么就会重新开辟一块内存,将3存入后,把a指向3所在的内存位置;

有个问题还没考证,就是不同的变量定义的值一样的时候,是不是指向的是同一块内存。在python中可以通过id来访问存放位置的id,在值(基本类型)较小的时候,就是指向同一块内存

有如下代码段(赋值引用类型)

1
2
var c={name:'阿Q'};
var d={name:'阿Q'};

上述代码定义了两个变量,定义c的时候在栈内存开辟一块空间存放一个内存地址,这个内存地址指向堆内存中放{name:'阿Q'}这个对象的内存;d同样如此,而且两个对象是毫无关系的,他们的内存地址是不一样的。
当修改变量的值的时候,如c={name:'老王'},这种情况就和基本类型一样,属于重新赋值,就会将对象{name:'老王'}存到堆内存中,将对象的地址存到栈内存中,c指向这个内存;
当修改变量,如c.name='老张',这种情况可以看作给对象做一个修正或者扩展,不属于重新赋值,虽然结果是修改了对象,但并没有改动对象所在的位置,即c指向的栈内存里存放的对象的地址并没有变;
当然如果如下定义

1
2
3
var c=d={name:'阿Q'};
// 或者
var a=b=1;

这样的连等赋值,有另外的说法,之后专写一篇关于连等赋值的问题。

函数传参是按值传递还是引用传递

对于这块内容,网上很多博文说法很多,还有说共享传递啥的,在此我们不对这些字面进行讨论,我们就解释下本质到底是怎么个情况。
比如有如下代码

1
2
3
4
5
6
var a = 1;
function foo(x) {
x = 2;
}
foo(a);
console.log(a); // 仍为1, 未受x = 2赋值所影响

这个就无需多解释了,x作为一个形参,只在函数内部生效,而且当a当作参数传给x的时候,其实就是复制了一份a指向的栈内存中存放的值,所以a和x就是毫无关系了
如果a为一个引用类型,如下代码

1
2
3
4
5
6
var obj = {x : 1};
function foo(o) {
o.x = 3;
}
foo(obj);
console.log(obj.x); // 3, 被修改了!

此处也一样,x作为一个形参,当obj当作参数传给o的时候,其实是复制了一份obj指向的栈内存中存放的对象的地址,所以obj和o在栈内存中是不一样的,但在栈内存中存的东西是一样的,就是对象{x : 1}所在的地址;
所以当修改o的时候,自然obj也就被修改了
还有人提出以下代码

1
2
3
4
5
6
var foo = {name:'foo'};
function test(o){
o = {name:'bar'};
}
test(foo);
console.log(foo.name); // foo,未被修改;

这个结果是没问题的,但这就是上文提到的对对象的修改的方式的问题,是直接赋值去修改变量,还是去修改对象的属性;
上述代码foo当作参数传给o,同样将对象所在地址赋值一份给了形参o,但函数中o的赋值操作,使形参o完全指向了另外一个对象的内存地址,这个操作,并不影响实参foo在内存中存放的地址,及地址所指向的对象;

参考资料

JS中函数的参数是按值传递还是按引用传递
JavaScript参数按值传递的理解
JS进阶系列之内存空间

本文转自ZengTianShengZ,您可以直接去原处阅览

第一章: 作用域是什么

1、 编译原理

JavaScript 被列为 ‘动态’ 或 ‘解释执行’ 语言,于其他传统语言(如 java)不同的是,JavaScript是边编译边执行的。
一段源码在执行前会经历三个步骤: 分词/词法分析 -> 解析/语法分析 -> 代码生成

  • 分词/词法分析

这个过程将字符串分解成词法单元,如 var a = 2; 会被分解成词法单元 var、 a、 = 、2、;。空格一般没意义会被忽略

  • 解析/语法分析

这个过程会将词法单元转换成 抽象语法树(Abstract Syntax Tree,AST)。
如 var a = 2; 对应的 抽象语法树 如下, 可通过 在线可视化AST 网址在线分析

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
{
"type": "Program",
"start": 0,
"end": 10,
"body": [
{
"type": "VariableDeclaration",
"start": 0,
"end": 10,
"declarations": [
{
"type": "VariableDeclarator",
"start": 4,
"end": 9,
"id": {
"type": "Identifier",
"start": 4,
"end": 5,
"name": "a"
},
"init": {
"type": "Literal",
"start": 8,
"end": 9,
"value": 2,
"raw": "2"
}
}
],
"kind": "var"
}
],
"sourceType": "module"
}
  • 代码生成

将 AST 转换成可执行的代码,存放于内存中,并分配内存和转化为一些机器指令

2、理解作用域

其实结合上面提到的编译原理,作用域就好理解了。作用域就是当前执行代码对这些标识符的访问权限。
编译器会在当前作用域中声明一些变量,运行时引擎会去作用域中查找这些变量(其实就是一个寻址的过程),如果找到这些变量就可以操作变量,找不到就往上一层作用域找(作用域链的概念),或者返回 null

第三章: 函数作用域和块作用域

1、函数中的作用域

每声明一个函数都会形成一个作用域,那作用域有什么用呢,它能让该作用域内的变量和函数不被外界访问到,也可以反过来说是不让该作用域内的变量或函数污染全局。

对比:

1
2
3
4
var a = 123
function bar() {
//...
}

1
2
3
4
5
6
function foo() {
var a = 123
function bar() {
//...
}
}

变量 a 和函数 bar 用一个函数 foo 包裹起来,函数 foo 会形成一个作用域,变量 a 和函数 bar 外界将无法访问,同时变量或函数也不会污染全局。

2、函数作用域

进一步思考,上面例子的变量 a 和函数 bar 有了作用域,但函数 foo 不也是暴露在全局,也对全局造成污染了啊。是的,JavaScript对这种情况提出了解决方案: 立即执行函数 (IIFE)

1
2
3
4
5
6
(function foo() {
var a = 123
function bar() {
//...
}
})()

第一个()将函数变成表达式,第二个()执行了这个函数,最终函数 foo 也形成了自己的作用域,不会污染到全局,同时也不被全局访问的到。

3、块作用域

es6之前JavaScript是没有块作用域这个概念的,这与一般的语言(如Java ,C)很大不同,看下面这个例子:

1
2
3
4
for (var i = 0; i < 10; i++) {
console.log('i=', i);
}
console.log('输出', i); // 输出 10

for 循环定义了变量 i,通常我们只想这个变量 i 在循环内使用,但忽略了 i 其实是作用在外部作用域(函数或全局)的。所以循环过后也能正常打印出 i ,因为没有块的概念。

甚至连 try/catch 也没形成块作用域:

1
2
3
4
5
6
try {
for (var i = 0; i < 10; i++) {
console.log('i=', i);
}
} catch (error) {}
console.log('输出', i); // 输出 10

解决方法1

形成块作用域的方法当然是使用 es6 的 let 和 const 了, let 为其声明的变量隐式的劫持了所在的块作用域。

1
2
3
4
for (let i = 0; i < 10; i++) {
console.log('i=', i);
}
console.log('输出', i); // ReferenceError: i is not defined

将上面例子的 var 换成 let 最后输出就报错了 ReferenceError: i is not defined ,说明被 let 声明的 i 只作用在了 for 这个块中。

除了 let 会让 for、if、try/catch 等形成块,JavaScript 的 {} 也能形成块

1
2
3
4
5
{
let name = '曾田生'
}

console.log(name); //ReferenceError: name is not defined

解决方法2

早在没 es6 的 let 声明之前,常用的做法是利用 函数也能形成作用域 这么个概念来解决一些问题的。

看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function () {
return i
}
}
console.log(i)// i 作用在整个函数,for 执行完此时 i 已经等于 10 了
return result
}
var result = foo()
console.log(result[0]()); // 输出 10 期望 0
console.log(result[1]()); // 输出 10 期望 1
console.log(result[2]()); // 输出 10 期望 2

这个例子出现的问题是执行数组函数最终都输出了 10, 因为 i 作用在整个函数,for 执行完此时 i 已经等于 10 了, 所以当后续执行函数 result[x]() 内部返回的 i 已经是 10 了。

利用函数的作用域来解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
var result = []
for (var i = 0; i < 10; i++) {
result[i] = function (num) {
return function () { // 函数形成一个作用域,内部变量被私有化了
return num
}
}(i)
}
return result
}
var result = foo()
console.log(result[0]()); // 0
console.log(result[1]()); // 1
console.log(result[2]()); // 2

上面的例子也是挺典型的,一般面试题比较考基础的话就会被问道,上面例子不仅考察到了块作用域的概念,函数作用域的概念,还考察到了闭包的概念(闭包后续讲但不影响这个例子的理解),多琢磨一下就理解了。

第四章: 提升

提升指的是变量提升和函数提升,为什么JavaScript会有提升这个概念呢,其实也很好理解,因为JavaScript代码是先 编译执行 的,所以在编译阶段就会先对变量和函数做声明,在执行阶段就出现了所谓的变量提升和函数提升了。

1、变量提升

1
2
console.log(a); // undefined
var a = 1;

上面代码 console.log(a); // undefined 就是因为编译阶段先对变量做了声明,先声明了个变量 a, 并默认赋值 undefined

1
2
3
var a;
console.log(a); // undefined
a = 1;

2、函数提升

函数同样也存在提升,这就是为什么函数能先调用后声明了

1
2
3
4
foo();
function foo() {
console.log('---foo----');
}

注意:函数表达式不会被提升

1
2
3
4
5
foo();
var foo = function() {
console.log('---foo----');
}
// TypeError: foo is not a function

注意:函数会首先被提升,然后才是变量

1
2
3
4
5
6
var foo = 1;
foo();
function foo() {
console.log('---foo----');
}
// TypeError: foo is not a function

分析一下,因为上面例子编译后是这样的

1
2
3
4
5
6
7
var foo = undefined; // 变量名赋值 undefined
function foo() { // 函数先提升
console.log('---foo----');
}
foo = 1; // 但接下去是变量被重新赋值了 1,是个Number类型
foo(); // Number类型当然不能用函数方式调用,就报错了
// TypeError: foo is not a function

第五章: 作用域闭包

闭包问题一直会在JavaScript被提起,是JavaScript一个比较奇葩的概念

1、闭包的产生

闭包的概念: 当函数可以记住并访问所在的词法作用域时,就产生了闭包

概念貌似挺简单的,简单分析下,首先闭包是 产生的,是在代码执行中产生的,有的一些网络博文直接将闭包定义为 某一个特殊函数 是错的。

闭包是怎么产生的呢,一个函数能访问到所在函数作用域就产生了闭包,注意到作用域的概念,咱们最上面的章节有提到,看下面例子:

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

var bat = foo()
bat() // 1
bat() // 2
bat() // 3

结合例子分析一下: 函数 foo 内部返回了函数 bar ,外部声明个变量 bat 拿到 foo 返回的函数 bar ,执行 bat() 发现能正常输出 1 ,注意前面章节提到的作用域,变量 a 是在函数 foo 内部的一个私有变量,不能被外界访问的,但外部函数 bat 却能访问的到私有变量 a,这说明了 外部函数 bat 持有函数 foo 的作用域 ,也就产生了闭包。

闭包的形成有什么用呢,JavaScript 让闭包的存在明显有它的作用,其中一个作用是为了模块化,当然你也可以利用外部函数持有另一个函数作用域的闭包特性去做更多的事情,但这边就暂且讨论模块化这个作用。

函数有什么作用呢,私有化变量或方法呀,那函数内的变量和方法被私有化了函数怎么和外部做 交流 呢, 暴露出一些变量或方法呀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
var _a = 0;
var b = 0;
function _add() {
b = _a + 10
}
function bar() {
_add()
}
function getB() {
return b
}
return {
bar: bar,
getB: getB
}
}

var bat = foo()
bat.bar()
bat.getB() // 10

上面例子函数 foo 可以理解为一个模块,内部声明了一些私有变量和方法,也对外界暴露了一些方法,只是在执行的过程中顺带产生了一个闭包

2、模块机制

上面提到了闭包的产生和作用,貌似在使用 es6语法 开发的过程中很少用到了闭包,但实际上我们一直在用闭包的概念的。

foo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var _a = 0;
var b = 0;
function _add() {
b = _a + 10
}
function bar() {
_add()
}
function getB() {
return b
}
export default {
bar: bar,
getB: getB
}

bat.js

1
2
3
4
import bat from 'foo'

bat.bar()
bat.getB() // 10

上面例子是 es6 模块的写法,是不是惊奇的发现变量 bat 可以记住并访问模块 foo 的作用域,这符合了闭包的概念。

小结:

本章节我们深入理解了JavaScript的 作用域提升闭包等概念,希望你能有所收获,这也是我在读《你不知道的JavaScript·上卷》的一些体会。下一部分整理下 this解析对象原型 等一些概念。

在项目的建立初期都是先搭建基础框架,在完成基础框架的搭建后,才把代码放到代码管理服务器上,让项目组成员进行检出从而进行需求的开发。而基础框架的搭建大部分都是先在个人的电脑上完成的,当完成框架搭建后,此时就要把代码共享到代码服务器上供开发人员检出。

准备工作

本地安装git,在远程建有仓库,由于我们是将本地代码推到远程仓库,所以建远程仓库的时候不要进行初始化,即不必在仓库下添加任何文件,如readme.mdd等。

本地仓库初始化

1
2
3
git init   // 初始化版本库
git add . // 添加文件到版本库(只是添加到缓存区),.代表添加文件夹下所有文件
git commit -m "first commit" // 把添加的文件提交到版本库,并填写提交备注

关联推送

1
2
3
git remote add origin 你的远程库地址  // 把本地库与远程库关联
git push -u origin master // 第一次推送时
git push origin master // 第一次推送后,直接使用该命令即可推送修改

之后可能需要输入用户名密码,或者是ssh的形式。

template和css中

在 template 的 img 标签等和 css 的背景图中

1
<img class="logo-img" src="~@/assets/logo/logo.png" key="max-logo" />
1
background: url(~@/assets/dongdong.png) 4px 4px no-repeat;

其中‘~’为的是让 webpack 识别这是一个路径,‘@’表示 webpack 中配置的路径别名,此处表示 src 文件夹

在js中

在 js 中作为变量引入时,需要以 require 的方式引入

1
2
3
default_avatar() {
return require('@/assets/avatar/avatar_' + this.name.length % 10 + '.png')
}

此处引入不需要前面写‘~’

assets与static的区别

  1. 静态文件(主要指图片)均放在了src文件夹下的assets下,此处考虑到引入的资源都是属于项目本身的文件;此处的文件会被webpack的插件处理,如压缩,base64,hash等;
  2. static中应放置公共的类库等,如jQuery;此处的文件会直接复制到输出文件夹dist下的static中,不会被webpack处理;引入时直接以绝对路径引入即可。

安装

在Windows下安装node很简单,直接取官网下载安装包,一路next就可以了,最新的安装包包含了npm,也会默认把node运行文件所在的目录添加到环境变量中。

当在终端执行node -v命令时,输出相应版本号说明安装成功。

配置

npm默认的模块缓存路径及全局安装路径都在C盘,习惯上我们自行指定一个位置,一般会把node安装在D盘,相应的路径也设置在node目录下

在nodejs的目录下提前创建好两个文件夹node_global和node_cache

如下命令设置路径:

1
2
npm config set prefix "D:\\nodejs\\node_global"
npm config set cache "D:\\nodejs\\node_cache"

设置完成后可通过npm config ls查看相关设置

此时需要将node_global路径加入到环境变量,这样保证终端可以找到全局安装的模块

注意

有些教程里会指导将全局模块的路径直接设置在nodejs目录下

1
npm config set prefix "D:\\nodejs"

全局安装的模块会安装在node_modules下,和默认安装的npm在一起,命令文件就在nodejs下,这样就不需要再添加环境变量了

但这种做法会带来一个问题就是,无法通过npm un npm@lstest -g来升级npm模块,因为npm无法再升级安装的时候覆盖自己,而正常的npm升级是在全局模块的路径安装最新的npm,所以按照开头的路径设置,在node在会有两个npm,一个是默认安装的,一个是升级的时候安装在全局路径的。

所以还是建议全局路径设置另外的文件路径,并添加至环境变量。