每日一题 (一)
本博客转自:「作者:若愚链接:https://zhuanlan.zhihu.com/p/22361337来源:知乎著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1、为什么document.write(1)有时候会重写文档,有时候不会?
当你打开一个页面,浏览器会:
1. 省略前面的步骤
2. 调用 document.open() 打开文档
3. document.write(...) 讲下载到的网页内容写入文档
4. 所有内容写完了,就调用 document.close()
5. 触发 dom ready 事件(DOMContentReady)
所以你如果在第四步之前 document.write(1) 那么你就直接追加内容到当前位置, 如果你在第4步之后 document.write(),那么由于 document 已经 close 了,所以必须重新 document.open() 来打开文档,这一打开,内容就被清空了。
不信你可以这样验证一下:
1. 打开 baidu.com 等页面加载完
2. 在控制台运行 document.write(1),会看到页面清空,只有一个 1
3. 再次运行 document.write(1),会发现页面没有清空,1 变成了 11,因为追加了一个1
4. 运行 document.close(),这是文档就关闭了。
5. 再次运行 document.write(1),你会发现文档又清空了,变成了 1。
2、为什么 document.all 有时像一个对象(数组),有时又不像一个对象(数组)?
document.all 奇怪的地方在于它是一个数组(其实是对象),但是类型却是undefined。
先看控制台代码
>>> document.all
HTMLAllCollection[1009] // 控制台打印出一个数组
>>> document.all[0]
<html>...</html> // 第一个元素是 html 标签,看起来很像一个数组
>>> typeof document.all
'undefined' // 咦,居然不是 object?
>>> if(document.all){console.log('document.all 为真')}else{console.log('document.all 为假')}
document.all 为假
![}IL3QCF08U]Q5)C8UY_{JGA.png](http:https://img.haomeiwen.com/i1181204/13ef440a4d07c023.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
用各个浏览器测试一下结果如下:
所有浏览器都支持用 document.all[index] 或者 document.all[id] 获取标签
但是 typeof document.all,在 IE 10 以下的值为 ‘object’,在其他浏览器的值均为 ‘undefined’
为何会这样?那我们就要从很久很久以前说起了。多久呢,大概20年前。
那个时候,浏览器刚刚出现,很多功能不完善,比如 document.getElementById,都有很多浏览器不支持。甚至连 W3C 都还没制定出 Web 标准,这个时候 IE 4推出了一些 API,只有 IE 4 支持,其中就包括我们今天说 document.all,它比document.getElementById 要好用一些,比如你可以用 document.all[‘topbar’] 来获取元素,所以那个时候很多程序员会些这样的代码:
if(document.all){
// IE 4 代码
document.all['topbar']
...
}else if(document.getElementById){
// 其他浏览器代码
document.getElementById('topbar')
}
甚至某些程序员直接认为 document.all 为真的浏览器,就是 IE。也就是这样:
if(document.all){
// IE
}else{
// 非 IE
}
其他浏览器(网景)觉得 IE 4 有的功能,我也要有才行。于是也加上 document.all,功能一样,可以获取元素。但是呢,其他浏览器又不想被某些程序员认为是 IE,于是 typeof document.all 的值定为 undefined。
有人说 W3C 标准出来后,为什么浏览器不把 document.all 纠正过来,去掉或者变成一个正常的对象都可以啊。太天真了,如果浏览器这么做了,那么当时世界上会有很多页面 JS 代码都不能正常运行,到时候用户只会怪浏览器不给力,然后卸载掉浏览器。
浏览器厂商才不想冒这样的风险。于是,最终,我们在所有浏览器上得到了一个类型为 undefined 的 document.all,只有 IE 10 以下,然后保留着 IE 4 的行为。
不过 document.all 现在已经被弃用了,大家不要再使用它了。用 document.getElementById 或者 document.querySelector 就好 !
3、为什么 0.1 + 0.2 结果为 0.30000000000000004?
如果你打开浏览器的控制台,输入以下代码并运行
Paste_Image.png
奇怪,为什么不是 0.3。这时可能你会去搜一下(很容易搜到结果),但是如果你了解计算机是如何存储小数(准确地说是浮点数)的话,很容易推断出原因。
首先我们要用以下尝试:
1.计算机将所有数据以二进制的形式存储
2.计算机用有限的大小来存储数据(因为现实生活中不存在无限大的内存或硬盘)
好的,然后结合我们的问题来看。
计算机如何存储 0.1 和 0.2?
如果你对十进制转二进制有兴趣可以看下图:
如果看不懂也可以直接看结论:十进制的 0.1 转为二进制,得到一个无限循环小数:0.00011…。
也就是说,二进制无法「用有限的位数」来表示 0.1。对于 0.2 也是一样的。二进制能「用有限的位数」表示的有:0.5、0.25、0.125 等。
但是计算机只能用有限的位数来存一个数,所以最终,计算机存的数是一个近似于 0.1 的小数。(具体转换过程参考这里)所以当我们计算 0.1 + 0.2 时,实际上算的是两个近似值相加,得到的值当然也是近似等于 0.3。
总结
1.问题的根源是十进制小数转为二进制小数的过程中,会损失精度
2.你在写代码的过程中,遇到小数都要小心,比如下面的代码会造成死循环:
var i = 0.1
while(i!=1){
console.log(i)
i += 0.1
}
因为 i 加 9 次 0.1,得到的值是 1 的近似值,并不是 1。
3.你应该对计算机存储方式有一定的了解。
4、var undefined = 1 这样赋值有效果吗?在什么情况下有?Why?
情景1:
var undefined = 1;
alert(undefined); // chrome: undefined, ie8: 1
alert(window.undefined);//undefined
Paste_Image.png
在 chrome 下运行得到的结果还是 「undefined」,但在IE8以下赋值是生效的。
可以看看 MDN 里关于这部分的描述: undefined - JavaScript
Paste_Image.png
情景2:
var obj = {};
obj.undefined = 'hanbaoyi';
console.log(obj.undefined) // hanbaoyi
在标准浏览器下作为全局作用域下 window的一个属性, undefined 不可修改;但对于一个普通对象,undefined可作为属性且可以修改。
情景3:
function fn(){
var undefined = 100;
alert(undefined); //chrome: 100, ie8: 100
}
fn();
不管是标准浏览器,还是老的 IE 浏览器,在函数内部 undefined 可作为局部变量重新赋值
情景4:
undefined = 100;
(function(global){
alert(undefined); //chrome: undefined, ie8: 100
})(window)
在标准浏览器下输出的结果是undefined。而在ie8下为100(因为可被赋值)
所以1.x版本的 jquery 中我们会看到类似这样的代码
(function(global, undefined){
alert(undefined)
})(window)
这样即使用户使用 IE8浏览器,在全局修改了undefined的值,在匿名函数内undefined 也不会发生变化。因为在匿名函数内实参只传递一个 window,而形参多了个undefined(可以把这个undefined当做一个普通参数名),这个未赋值的变量在匿名函数内就是真正的undefined。
undefined 的其他边边角角
有时候我们需要判断一个变量是不是undefined,会这样用
但假如 str 这个变量没声明就会出现报错,用下面的方式会更好一些
Paste_Image.png有时候我们会看到这种写法
if(str === void 0){
console.log('I am real undefind');
}
那是因为 「void 0」的执行结果永远是「undefined」, 即使在某些老旧浏览器 或者在某个函数中 undefined被重新赋值,我们仍然可以通过 「void 0」 得到真正的「undefined」。
Paste_Image.png5、button.disabled 和 button.getAttribute('disabled') 有什么区别?
「node.getAttribute('someAttribute')」获取的是「attribute」,而「node.someAttribute」获取的是元素的「property」,二者并不相同。 参考 properties-and-attributes-in-html
在大多数情况下「property」和「attribute」是同步的,如场景1。
场景1:
<input id="username" type="text">
<script>
var userInput = document.querySelector('#username');
console.log( userInput.id ); //"username"
console.log( userInput.getAttribute('id') ); //"username"
</script>
Paste_Image.png
这里 userInput.id 和 userInput.getAttribute('id') 获取的值相等。
当然我们关注的是例外,如场景2、3、4.
场景2:
<input id="username" type="text" sex="male" age=26>
<script>
var userInput = document.querySelector('#username');
console.log( userInput.sex ); // undefined
console.log( userInput.getAttribute('sex') ); // "male"
console.log( userInput.getAttribute('SEX') ); // "male"
console.log( userInput.getAttribute('age')); // "26"
</script>
Paste_Image.png
从上面的例子可以得出如下结论:
- 「node.property」的方式不能获取自定义属性,「node.getAttribute()」的方式可以获取自定义属性
- 「node. getAttribute()」获取自定义属性忽略属性的大小写
- 「node.getAttribute()」获取自定义属性得到的值的类型总是字符串
场景3:
Paste_Image.png Paste_Image.png对于上面的例子,HTML中只要出现了disabled 属性,不管值是什么,对于 DOM property结果都是true, 而对于 attribute 获取的则是把 HTML 里对应属性的值拿到转换成字符串。
input 标签的 checked 也有类似的特性。
场景4:
Paste_Image.png对于 a 链接的 href, 使用 a.getAttribute('href') 就是从 HTML 里获取对应属性的值转化成字符串,而 a.href 则获取有意义的真实地址。
场景5:
Paste_Image.png对于input 的 value, 改变 property 不会同步到 atttribute 上,改变 attribute也不会同步到 value上, attribute对应 HTML, property 对应 DOM。
那到底用哪一种呢?
如果你只是想获取非自定义的属性,比如 id、name、src、href 、checked... 用 property 的方式比较符合日常习惯,如果需要获取自定义属性那只能使用 getAttribute。当然具体用哪一种你只要了解二者的区别,大胆选用吧~
6、为什么不建议将 font-size 设置为 12px 以下?如果一定要设置为 12px 以下要怎么做?
先看看把 font-size 设置为 12px 以下时的效果:(浏览器为 Chrome 52)
Paste_Image.png
在其他浏览器上效果却不一样:
Paste_Image.png
因为 Chrome 这款任性的浏览器做了如下限制:
-
font-size 有一个最小值 12px(不同操作系统、不同语言可能限制不一样),低于 12px 的,一律按 12px 显示。理由是 Chrome 认为低于 12px 的中文对人类的不友好的。
-
但是允许你把 font-size 设置为 0.
-
这个 12px 的限制用户是可以自行调整的,进入 chrome://settings/fonts 设置,滚动到最下方你就可以调整 12px 为其他值。
如果我一定要设置小于 12px 的字体怎么办?
-
Chrome 29 版本之前,你可以使用
-webkit-text-size-adjust: none;
来解除这个限制。29 版本后,就不能这样做了。 -
你可以先设置 12px,然后使用 transform: scale(0.833333) 讲元素缩小,效果跟 10px 很接近。不过要注意的是,transform: scale chu了缩小 font-size,也会缩小其他一些属性,需要多测试。
7、cookie、session、localStorage分别是什么?有什么作用?
<b>
Cookie 是什么</b>
-
Cookie 是浏览器访问服务器后,服务器传给浏览器的一段数据。用来记录某些当页面关闭或者刷新后仍然需要记录的信息。在控制台用 「document.cookie」查看你当前正在浏览的网站的cookie。
-
cookie可以使用 js 在浏览器直接设置(用于记录不敏感信息,如用户名), 也可以在服务端通使用 HTTP 协议规定的 set-cookie 来让浏览器种下cookie,这是最常见的做法。(打开一个网站,清除全部cookie,然后刷新页面,在network的Response headers试试找一找set-cookie吧)
-
每次网络请求 Request headers 中都会带上cookie。所以如果 cookie 太多太大对传输效率会有影响。
4.一般浏览器存储cookie 最大容量为4k,所以大量数据不要存到cookie。
5.设置cookie时的参数:
path:表示 cookie 影响到的路径,匹配该路径才发送这个 cookie。expires 和 maxAge:告诉浏览器 cookie 时候过期,maxAge 是 cookie 多久后过期的相对时间。不设置这两个选项时会产生 session cookie,session cookie 是 transient 的,当用户关闭浏览器时,就被清除。一般用来保存 session 的 session_id。
secure:当 secure 值为 true 时,cookie 在 HTTP 中是无效,在 HTTPS 中才有效
httpOnly:浏览器不允许脚本操作 document.cookie 去更改 cookie。一般情况下都应该设置这个为 true,这样可以避免被 xss 攻击拿到 cookie。
<b>
如何使用 Cookie</b>
Cookie 一般有两个作用。
- 第一个作用是识别用户身份。
比如用户 A 用浏览器访问了 http://a.com,那么 http://a.com 的服务器就会立刻给 A 返回一段数据「uid=1」(这就是 Cookie)。当 A 再次访问 http://a.com的其他页面时,就会附带上「uid=1」这段数据。
同理,用户 B 用浏览器访问 http://a.com时,http://a.com 发现 B 没有附带 uid 数据,就给 B 分配了一个新的 uid,为2,然后返回给 B 一段数据「uid=2」。B 之后访问 http://a.com的时候,就会一直带上「uid=2」这段数据。
借此,http://a.com 的服务器就能区分 A 和 B 两个用户了。 - 第二个作用是记录历史。
假设 http://a.com 是一个购物网站,当 A 在上面将商品 A1 、A2 加入购物车时,JS 可以改写 Cookie,改为「uid=1; cart=A1,A2」,表示购物车里有 A1 和 A2 两样商品了。这样一来,当用户关闭网页,过三天再打开网页的时候,依然可以看到 A1、A2 躺在购物车里,因为浏览器并不会无缘无故地删除这个 Cookie。
借此,就达到里记录用户操作历史的目的了。
session
-
当一个用户打开淘宝登录后,刷新浏览器仍然展示登录状态。服务器如何分辨这次发起请求的用户是刚才登录过的用户呢?这里就使用了session保存状态。用户在输入用户名密码提交给服务端,服务端验证通过后会创建一个session用于记录用户的相关信息,这个 session 可保存在服务器内存中,也可保存在数据库中。
cookie 是存储在浏览器里的一小段「数据」,而session是一种让服务器能识别某个用户的「机制」,session 在实现的过程中需要使用cookie。 二者不是同一维度的东西。 -
创建session后,会把关联的session_id 通过setCookie 添加到http响应头部中。
浏览器在加载页面时发现响应头部有 set-cookie字段,就把这个cookie 种到浏览器指定域名下。当下次刷新页面时,发送的请求会带上这条cookie, 服务端在接收到后根据这个session_id来识别用户。
localStorage
-
localStorage HTML5本地存储web storage特性的API之一,用于将大量数据(最大5M)保存在浏览器中,保存后数据永远存在不会失效过期,除非用 js手动清除。
-
不参与网络传输。
-
一般用于性能优化,可以保存图片、js、css、html 模板、大量数据。
8、JS里基本类型(值)和复杂类型(引用)有什么区别?
Paste_Image.png短答案:
-
基本类型变量存的是值,复杂类型的变量存的是内存地址。
-
基本类型在赋值的时候拷贝值,复杂类型在赋值的时候只拷贝地址,不拷贝值。
长答案:
在讲解这个问题之前,我们需要看一下计算机是如何储存变量的。
如果计算机要存储一个整数,它可以用 32 个位(bit)来存储,一个小数,可以用 64 位来存储。但不管怎样,位数都是固定的。
假设有如下 JS 代码:
var a = 1.23;
var b = 3.14;
对应的内存是这样:
Paste_Image.png
接下来加几行代码:
var obj = {}
var c = 1.628
现在,计算机遇到一个难题,到底用多少位来存 obj 呢?
Paste_Image.png
用固定位数有一个问题,如果程序员之后往 obj 上添加属性(如 obj.a = 1.23; obj.b = 2.34),那么 obj 用的位数可能放不下这两个小数。
有人说,「那给 obj 分配足够多的位数不就好了,比如一万位,总够用吧?」不行,这样做一来浪费内存,二来一万位也不一定够用,万一 obj 里面的属性非常多呢?
引入另一种内存
为了解决 obj 存储的问题,计算机将程序里的内存分成两种,一种是上图所示,按顺序使用用内,每个数据占据的位数是固定的,这种内存叫做「栈内存」;另一个,就是专门用来存储位数不固定的数据,存的时候不一样按顺序一个一个存,这种内存叫做「堆内存」。
我们来看 obj 应该怎么存储:
Paste_Image.png
如图, obj 在栈内存那里,只占固定位数(32位或64位或其他都可以),里面存的并不是数据{a:1.23,b:2.34},里面存的是数据「在内存中的位置」(类似于引用或者指针)。
堆内存里,会开辟一块空间来存放 {a:1.23, b:2.34}。
「如果我再给 obj 添加一个属性怎么办呢?obj.c = 3.45」
好问题,那么 obj.c 依旧会放到堆内存,同时占用的内存空间也会动态的变化,以盛放 obj.a、obj.b 和 obj.c 三个小数。具体怎么动态变化,则是由 JS 引擎来负责的,暂且不表。
值 V.S. 引用
如果变量存储的是原始值,那么这个变量就是值类型,在 JS 里也叫做基本类型。
如果变量存储的是内存位置,那么这个变量就是引用类型,在 JS 里也叫复杂类型,也就是对象。
值类型在赋值的时候是直接拷贝的,而引用类型则只拷贝地址。
值类型的赋值举例:
var a = 1.23
var b = a
对应的内存结果为:
Paste_Image.png
引用类型的赋值举例:
var a = {name: 'a'}
var b = a
对应内存结果为:
Paste_Image.png
也就是说,a 和 b 都存储着「同一块内存」的地址!那么就会有一个问题:
当我们修改 b.name 的时候,a.name 也会跟着变:
b.name = 'b'
a.name === 'b' // true
以上,就是对「值」和「引用」的区别的浅析。
9、如何在不刷新页面的情况下改变URL?
问没有具体业务场景的技术问题都是耍流氓,那在回答这个问题之前先简单介绍一下业务场景。
下午6点半,小 H写了一个下午的代码揉揉眼睛伸个懒腰,「今天终于能早点回去了,先刷会知乎歇会」。突然,产品 小U一脸淫笑飘了过来,小 H 略感不妙。『嗨嗨~ 你这工作状态不饱和啊,有个小需求来看看。现在需要做一个新闻展示页,主功能区块分为新闻列表和分页两部分。很简单,两天能搞定吧』小 U 说。小 H 看了看原型稿,心想确实不难。点击分页时把直接把分页参数传递给后台,页面刷新后台直接返回渲染后的数据就行了,模板写的好的话甚至 js 都不需要了。正当小 H 开口准备说说技术实现时,被小 U 打断...『不过为了体验好一些,在用户点下一页的时候别刷新页面』小 U 说。『不刷新页面没关系,我用 ajax 可以实现,不过时间嘛...』小 H 略有所思『果然是大牛啊,能实现我就放心了。时间好商量,不过这个项目特别急,晚上加加油啊』,说完小 U 就飘走了。『cao, 看来又走不成了』小 H 嘀咕着。两分钟后小 U 又跑了过来,『刚才忘了跟你说了,用户点了下一页后地址栏的地址要跟着变,这时候刷新页面还能定位到当前页』『 🐎x10000~~~』
整理下需求:
- 点击分页页码可实现无刷新页面加载
- 同时 URL 在数据加载后会发生变化展示对应页码
- 刷新页面(带页码参数)会定位到当前页码
效果如Demo 所示。
-
对于第1条,我们可以使用 ajax 动态获取对应页码的数据。
-
对于第2条,我们可以使用 html5的 api「history.pushState」,用于改变 URL。
-
对于第3条,我们可以根据 URL 中页码参数获取对应页码的数据再做展示。
那history.pushState如何使用呢?比如当用户点击页码按钮时,可使用 ajax 获取对应页码的数据,拼装 DOM 放到页面上,然后调用下面的 setUrl 方法实现浏览器 URL 的更新。function setUrl(page){ var url = location.pathname + '?page=' + page history.pushState({url: url, title: document.title}, document.title, url)
}
history.pushState() 带有三个参数:一个状态对象,一个标题(现在被忽略了),以及一个可选的URL地址。
state object — 状态对象是一个由 pushState()方法创建的与历史纪录相关的JS对象。
title — 火狐浏览器现在已经忽略此参数,将来也许可能被使用。考虑到将来有可能的改变,传递一个空字符串是安全的做法。当然,你可以传递一个短标题给你要转变成的状态。
URL — 这个参数提供了新历史纪录的地址。请注意,浏览器在调用pushState()方法后不会去加载这个URL,但有可能在之后会这样做,比如用户重启浏览器之后。新的URL可以是绝对地址,也可以是相对地址。新URL必须和当前URL在同一个源下。
想看实现效果?参考这里一个无刷新分页的 DEMO** ,建议看看源码实现。
10、Fetch API 是什么?能代替 AJAX 吗?
短答案:
Fetch 是浏览器提供的原生 AJAX 接口。使用 window.fetch 函数可以代替以前的 $.ajax、$.get 和 $.post。
长答案:
前端发展地越来越快,我们用了好几年的 $.ajax,居然也渐渐变得过时了。
以前我们用 jQuery.ajax 发一个请求是这样的:
$.ajax('/').then(function(response){
console.log(response)
}
现在我们用 fetch 发一个请求是这样的:
fetch('/').then(function(response){
response.text().then(function(text){
console.log(text)
})
})
是不是感觉很像,像就对了,因为 Fetch API 就是浏览器提供的用来代替 jQuery.ajax 的工具。
AJAX 的原理
我们知道 jQuery.ajax 是使用 XMLHttpRequest 对象来发送异步请求的。
var xhr = new XMLHttpRequest()
xhr.onreadystatechange = function(){
if(xhr.readyState === 4 && xhr.status === 200){
console.log(xhr.responseText)
}
};
xhr.open('GET',url,true)
xhr.send()
是不是很麻烦,是不是?
正是由于 XMLHttpRequest 使用起来相当麻烦,所以大家才喜欢使用 jQuery 提供的 $.ajax 方法。
用 fetch 代替 $.ajax
随着 React.js、Angular.js 和 Vue.js 这些前端框架的流行,很多单页面应用已经不再使用 jQuery 了,这意味着你要自己对 XMLHttpRequest 进行封装,而很多人选择封装一个跟 jQuery.ajax 差不多的接口。
Fetch API 的出现,就是为了给类似的操作流程定一个接口规范。
换句话说,就是浏览器帮你把 jQuery.ajax 给实现了,以后大家都是用 fetch 来发送异步请求就好了。
Fetch API 提供的一组对象
window.fetch 函数只是 Fetch API 提供的众多接口中的一个,还有很多有用的对象:
- window.Headers
- window.Response
- window.Request
……
要学的东西又多了起来……
Fetch API 的特点
- 基于 Promise(如果你没有学过 Promise,强烈建议你学一学)
- 不需要依赖第三方库,就可以优雅地使用 AJAX
Fetch API 的问题
- 使用 fetch 无法取消一个请求。这是因为 Fetch API 基于 Promise,而 Promise 无法做到这一点。不过相信很快就会有对策。
兼容性
有的浏览器没有 Fetch API,没有关系,只要引入一个 polyfill 就可以了:GitHub - github/fetch: A window.fetch JavaScript polyfill.
更多Fetch详解请看 Fecth MDN
11、什么是立即执行函数?有什么作用?
这是 JS 中的一个常见概念,面试时经常会被问到,请「用自己的语言」简述
- 立即执行函数是什么?
- 立即执行函数有什么用途?
回答:
1. 立即执行函数是什么
立即执行函数就是:
-
声明一个匿名函数
-
马上调用这个匿名函数
Paste_Image.png
上面是一个典型的立即执行函数。
-
首先声明一个匿名函数 function(){alert('我是匿名函数')}。
-
然后在匿名函数后面接一对括号 (),调用这个匿名函数。
Paste_Image.png
那么为什么还要用另一对括号把匿名函数包起来呢?
其实是为了兼容 JS 的语法。
如果我们不加另一对括号,直接写成
function(){alert('我是匿名函数')}()
浏览器会报语法错误。
想要通过浏览器的语法检查,必须加点小东西,比如下面几种:(function(){alert('我是匿名函数')} ()) // 用括号把整个表达式包起来(function(){alert('我是匿名函数')}) () //用括号把函数包起来 !function(){alert('我是匿名函数')}() // 求反,我们不在意值是多少,只想通过语法检查。 +function(){alert('我是匿名函数')}() -function(){alert('我是匿名函数')}() ~function(){alert('我是匿名函数')}() void function(){alert('我是匿名函数')}() new function(){alert('我是匿名函数')}()
2、 立即执行函数有什么用?
只有一个作用:创建一个独立的作用域。这个作用域里面的变量,外面访问不到(即避免「变量污染」)。以一个[著名的面试题](https://link.zhihu.com/? target=http%3A//js.jirengu.com/didu/1)为例:
var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
liList[i].onclick = function(){
alert(i) // 为什么 alert 出来的总是 6,而不是 0、1、2、3、4、5
}
}
为什么 alert 的总是 6 呢,因为 i 是贯穿整个作用域的,而不是给每个 li 分配了一个 i,如下:
Paste_Image.png
那么怎么解决这个问题呢?用立即执行函数给每个 li 创造一个独立作用域即可(当然还有其他办法):
var liList = ul.getElementsByTagName('li')
for(var i=0; i<6; i++){
(function(ii){
liList[ii].onclick = function(){
alert(ii) // 0、1、2、3、4、5
}
})(i)
}
在立即执行函数执行的时候,i 的值被赋值给 ii,此后 ii 的值一直不变。i 的值从 0 变化到 5,对应 6 个立即执行函数,这 6 个立即执行函数里面的 ii 「分别」是 0、1、2、3、4、5。
以上,就是立即执行函数的基本概念。
12、谈谈你对原型、原型链、 Function、Object 的理解?
问题
有如下代码:
- 问题1:画出代码1的原型图?
- 问题2:从代码2你能得出什么结论?试画出原型图?
- 问题3:解释代码3的原因?
解答:
在解答上面的问题之前,先记住下面几句话,这几句话能解释一切关于原型方面的问题:
当 new 一个函数的时候会创建一个对象,『函数.prototype』 等于 『被创建对象.proto』
一切函数都是由 Function 这个函数创建的,所以『Function.prototype === 被创建的函数.proto』
一切函数的原型对象都是由 Object 这个函数创建的,所以『Object.prototype === 一切函数.prototype.proto』
下面是代码1的原型图:
Paste_Image.png- (1)People函数创建了对象 p,所以People.prototype === p.proto;
- (2)Object函数创建了People.prototype对象,所以Object.prototype === People.prototype.proto;
- (3)People 作为对象的角色被函数Function创建,所以 Function.prototype === People.proto
下面是代码2的原型图:
Paste_Image.png
- (1)任何函数都是 Function 创建,所以Function 创建了 Function,所以 Function.prototype === Function.proto;
- (2)Object 也是函数。所以Function创建了Object,所以 Function.prototype === Object.proto ;
- (3)Function.prototype 是普通对象,普通对象是由Object创建的,所以 Function.prototype.proto === Object.prototype
关于代码3:
instanceof 的作用是判断一个对象是不是一个函数的实例。比如 obj instanceof fn, 实际上是判断fn的prototype是不是在obj的原型链上。比如: obj.proto === fn.prototype, obj.proto.proto === fn.prototype,obj.proto...proto_ === fn.prototype,只要一个成立即可。
所以(根据图2来找)
- 对于 Function instanceof Function,因为 Function.proto === Function.prototype,所以为true。
- 对于 Object instanceof Object, 因为 Object.proto.proto === Function.prototype.proto === Object.prototype , 所以为true
- 对于 Function instanceof Object, 因为 Function.proto.proto === Function.prototype.proto === Object.prototype, 所以为 true
- 对于 Object instanceof Function, 因为 Object.proto === Function.prototype,所以为 true
13、JS 中的闭包是什么?
大名鼎鼎的闭包!这一题终于来了,面试必问。请用自己的话简述
- 什么是「闭包」。
- 「闭包」的作用是什么。
首先来简述什么是闭包
为简明起见,上面代码不使用全局变量,你可以假设上面三行代码在一个立即执行函数中。上图一个三行代码,一个局部变量 local,一个函数 foo,foo 里面可以访问到 local 变量。好了这就是一个闭包:
「函数」和「函数内部能访问到的变量」(也叫环境)的总和,就是一个闭包。
就这么简单。
有的同学就疑惑了,闭包这么简单么?「我听说闭包是需要函数套函数,然后 return 一个函数的呀!」,比如这样:
function foo(){
var local = 1
function bar(){
local++
return local
}
return bar
}
var func = foo()
func()
这里面确实有闭包,local 变量和 bar 函数就组成了一个闭包(Closure)。
为什么要函数套函数呢?
是因为需要局部变量,所以才把 local 放在一个函数里,如果不把 local 放在一个函数里,local 就是一个全局变量了,达不到使用闭包的目的——隐藏变量(等会会讲)。
有些人看到「闭包」这个名字,就一定觉得要用什么包起来才行。其实这是翻译问题,闭包的原文是 Closure,跟「包」没有任何关系。
所以函数套函数只是为了造出一个局部变量,跟闭包无关。
为什么要 return bar 呢?
因为如果不 return,你就无法使用这个闭包。把 return bar 改成 window.bar = bar 也是一样的,只要让外面可以访问到这个 bar 函数就行了。所以 return bar 只是为了 bar 能被使用,也跟闭包无关。
闭包的作用
闭包常常用来「间接访问一个变量」。换句话说,「隐藏一个变量」。
假设我们在做一个游戏,在写其中关于「还剩几条命」的代码。如果不用闭包,你可以直接用一个全局变量:
window.lives = 30 // 还有三十条命
这样看起来很不妥。万一不小心把这个值改成 -1 了怎么办。所以我们不能让别人「直接访问」这个变量。怎么办呢?
-
用局部变量。
-
但是用局部变量别人又访问不到,怎么办呢?
暴露一个访问器(函数),让别人可以「间接访问」。
代码如下:!function(){ var lives = 50 window.奖励一条命 = function(){ lives += 1 } window.死一条命 = function(){ lives -= 1 } }()
那么在其他的 JS 文件,就可以使用 window.奖励一条命() 来涨命,使用 window.死一条命() 来让角色掉一条命。
看到闭包在哪了吗?
Paste_Image.png 闭包是 JS 函数作用域的副产品。换句话说,正是由于 JS 的函数内部可以使用函数外部的变量,所以这段代码正好符合了闭包的定义。而不是 JS 故意要使用闭包。
很多编程语言也支持闭包,另外有一些语言则不支持闭包。
只要你懂了 JS 的作用域,你自然而然就懂了闭包,即使你不知道那就是闭包!
所谓闭包的作用。
那么请问,这算是闭包的作用吗?
-
关于闭包的谣言,闭包会造成内存泄露?
错。
说这话的人根本不知道什么是内存泄露。内存泄露是指你用不到(访问不到)的变量,依然占居着内存空间,不能被再次利用起来。闭包里面的变量明明就是我们需要的变量(lives),凭什么说是内存泄露? -
这个谣言是如何来的?
因为 IE。IE 有 bug,IE 在我们使用完闭包之后,依然回收不了闭包里面引用的变量。
这是 IE 的问题,不是闭包的问题。参见司徒正美的这篇文章**。
14、XSS 是什么?
新人经常在不知不觉中写出一个 XSS 漏洞,甚至连老司机也偶有湿鞋。请用自己的语言简述:
- XSS 是什么(举例说明)
- 如何防治 XSS
XSS 是什么?
是英文 Cross-Site Scripting 的缩写。
简单来说
- 正常用户 A 提交正常内容,显示在另一个用户 B 的网页上,没有问题。
- 恶意用户 H 提交恶意内容,显示在另一个用户 B 的网页上,对 B 的网页随意篡改。
造成 XSS 有几个要点:
1、恶意用户可以提交内容
2、提交的内容可以显示在另一个用户的页面上
3、这些内容未经过滤,直接运行在另一个用户的页面上
举例说明
假设我们有一个评论系统。
用户 A 提交评论「小谷你好」到服务器,然后用户 B 来访问网站,看到了 A 的评论「小谷你好」,这里没有 XSS。
恶意用户 H 提交评论「<script>console.log(document.cookie)</script>」,然后用户 B 来访问网站,这段脚本在 B 的浏览器直接执行,恶意用户 H 的脚本就可以任意操作 B 的 cookie,而 B 对此毫无察觉。有了 cookie,恶意用户 H 就可以伪造 B 的登录信息,随意访问 B 的隐私了。而 B 始终被蒙在鼓里。
XSS 的成因以及如何避免?
继续上面例子,之所以恶意脚本能直接执行,有两个可能
-
后台模板问题
<p>
评论内容:<?php echo $content; ?>
</p>
$content 的内容,没有经过任何过滤,原样输出。
要解决这个原因,只需要后台输出的时候,将可疑的符号 < 符号变成 '<'; (HTML实体)就行。 -
前端代码问题
$p.html(content)
或者
$p = $('<p>'+ content +'</p>')
content 内容又被原样输出了。解决办法就是不要自己拼 HTML,尽量使用 text 方法。如果一定要使用 HTML,就把可疑符号变成 HTML 实体。
示例代码
以上,就是 XSS 的简单介绍。
15、CSRF 是什么?
先从一个故事说起(故事纯属虚构,恶意模仿后果自负):
小谷最近遭遇电信诈骗被骗的倾家荡产,于是他想到了报复社会。在知乎上狂点800个没有帮助1000个举报后,他决定做点正事干票捞钱的生意。
「很多视频网站都有赠送礼品的功能,假如所有人都赠送我个礼物,我再转卖掉,不就发财啦」小谷寻思着。
说干就敢,经过一番折腾测试,小谷发现视频网站赠送礼物的接口是:
https://xxxx.com/gift/send?target=someone&giftId=ab231
原来只要用户在登录状态下请求这个地址,就能给名为someone的用户赠送礼品ab231。那如何才能让其他用户请求这个接口呢?其实只要用户点击就行,「色诱是最好的陷阱」回想起自己被骗的经过,小谷猥琐狠狠的打了一行文字 ——「想要的都在这里,今夜注定让你无眠~~],然后在各个群组里回复。
经过一天等待,有几个上钩的,但远远达不到预期,「有没有更自动的办法,让用户只要看到即使不点也能上钩呢?」。小谷开始对整个网站功能逐一过滤,突然他眼前一亮,在用户评论编辑框内看到了上传外链图片的功能。「如果把这个 url 作为图片填进去,上传后会在评论区创建一个img 标签,src 对应的就是这个地址。当用户打开页面后这个 img 就会自动加载图片也就是发送这个请求,这样一来凡是打开这个页面的人不论是不是点击这个链接都会给赠送礼物,perfect!」 小谷为自己的聪明才智惊叹。
又过了一天,果然源源不断的礼物送了过来。这个时候小谷隐隐有些担心起来,虽然礼物送的都很小,但赠送都是有历史记录的,用户查看历史记录肯定会起疑心,能不能帮用户删除这条赠送记录呢?经过测试,发现删除赠送记录的接口地址是
https://xxxx.com/gift/deleteRecord
接口类型为POST,请求参数为 { giftId:"ab231"}。 用户无法通过点击一个链接在不知情的情况下发送 POST 请求,怎么办呢?于是,小谷构造了一个页面:
<body>
哈哈,给你开了个玩笑,莫生气~
<iframe name="hiddenIframe" style="display:none"></iframe>
<form action="https://xxxx.com/gift/deleteRecord" id="form" method="post" style="visibility:hidden" target="hiddenIframe">
<input type="text" name="giftId" value="ab231">
</form>
<script>
document.getElementById('form').submit();
location.href = "http://xxxx.com";
</script>
</body>
当用户点开这个页面的链接后,会自动发送 POST 请求,然后跳转到原始首页。这样用户既在不知情的情况下赠送了礼品,又在不知情的情况下删除了赠送记录。大功告成后,小谷购买了个广告机在各大论坛狂发....一周之后,警察👮叔叔来敲门了,咚🕳🕳
上面小谷的攻击流程就是典型的 CSRF (Cross Site Request Forgery)攻击,中文名:跨站请求伪造。其原理是攻击者构造网站后台某个功能接口的请求地址,诱导用户去点击或者用特殊方法让该请求地址自动加载。用户在登录状态下这个请求被服务端接收后会被误以为是用户合法的操作。对于 GET 形式的接口地址可轻易被攻击,对于 POST 形式的接口地址也不是百分百安全,攻击者可诱导用户进入带 Form 表单可用POST方式提交参数的页面。
后续......
xxxx视频网站不断接到用户举报,自己的礼品莫名丢失。经过排查发现有攻击者利用 CSRF 进行攻击,报警后赶紧让公司的安全部门的小饥来修复漏洞。
小饥梳理了一遍公司网站所有的接口,发现很多接口都存在这个问题。于是采用了anti-csrf-token的方案。 具体方案如下:
-
服务端在收到路由请求时,生成一个随机数,在渲染请求页面时把随机数埋入页面(一般埋入 form 表单内,<input type="hidden" name="_csrf_token" value="xxxx">)
-
服务端设置setCookie,把该随机数作为cookie或者session种入用户浏览器
-
当用户发送 GET 或者 POST 请求时带上_csrf_token参数(对于 Form 表单直接提交即可,因为会自动把当前表单内所有的 input 提交给后台,包括_csrf_token)
-
后台在接受到请求后解析请求的cookie获取_csrf_token的值,然后和用户请求提交的_csrf_token做个比较,如果相等表示请求是合法的。
Paste_Image.png
(上图是某电商网站的真实设置,这里页面上设置的 token和session里设置的token 虽然不直接相等,但 md5('1474357164624') === '4bd4e512b0fbd9357150649adadedd4e',后台还是很好计算的)
安全部的Leader 看了看小饥的方案,「方案出的很赞, 不过还有几点需要注意一下」:
- Token 保存在 Session 中。假如 Token 保存在 Cookie 中,用户浏览器开了很多页面。在一些页面 Token 被使用消耗掉后新的Token 会被重新种入,但那些老的 Tab 页面对应的 HTML 里还是老 Token。这会让用户觉得为啥几分钟前打开的页面不能正常提交?
- 尽量少用 GET。假如攻击者在我们的网站上传了一张图片,用户在加载图片的时候实际上是向攻击者的服务器发送了请求,这个请求会带有referer表示当前图片所在的页面的 url。 而如果使用 GET 方式接口的话这个 URL 就形如:
https://xxxx.com/gift?giftId=aabbcc&_csrf_token=xxxxx
,那相当于攻击者就获取了_csrf_token,短时间内可以使用这个 token 来操作其他 GET 接口。
16、什么是 Web 服务器(server)?
首先我们来了解什么是服务器(server)
一般来说,server 有两重意思:
- 有时候 server 表示硬件,也就是一台机器。它还有另一个名字:「主机」。
- 更多时候,server 表示软件程序,这种程序主要用来对外提供某些服务,比如邮件服务、FTP 服务、数据库服务、网页服务等。
作为开发者,我们说 server 的时候,一般指的后者,也就是一个 24 小时运行的软件程序。
一台主机上面可以运行多个这样的程序。
什么是 Web Server?
顾名思义,Web Server 就是提供 Web 服务的 Server。
比如我们访问 http://baidu.com, 其实就是在使用百度的 Server 提供的服务。
一般来说, Web Server 对外提供的是 HTTP 服务(也可以是其他服务),这就是为什么我们的网址都以http:// 开头。
如何提供 HTTP 服务?
下面是由Node.js 写的一个最简单的 HTTP server
// 文件名 index.js
// 使用 node index.js 可运行本程序
var http = require('http')
var server = http.createServer( function (request, response){
response.end('这是页面内容,你请求的路径是:' + request.url)
})
server.listen(8080, function(){ console.log("正在监听 %s 端口", 8080);});
你不用看懂这段程序,你只需要知道两件事情:
- 1、这段程序监听了当前机器的 8080 端口。
- 2、一旦外部访问当前机器的 8080 端口,这段程序就会返回一段文字。
这就是一个最简单的 HTTP server。
分类
提供 HTTP 服务的 server 分为两类。
- 静态文件服务器
这种服务器简单地根据访问路径,返回对应的文件。
比如用户访问 http:// 123.123.123.123:8080/a/b/c/d.html,那么这种服务器就会在网站根目录找到 a/b/c/d.html 文件,原样返回给用户。 - 动态内容服务器
这种服务器返回的内容一般不是文件,而是动态生成的字符串(比如从数据库中获取的字符串)。
比如用户访问 http://weibo.com/home, 那么这种 http://weibo.com 的服务器则会返回当前用户最新的微博消息。显然每个用户得到的内容是不一样的。
以上,就是 Web 服务器的简单描述。
17、Proxy 对象是做什么用的?
打开 Chrome 控制台,输入 window.Proxy ,你会发现 JavaScript 已经内置了一个全局的 Proxy 对象,请问这个对象是做什么用的?
其实你用关键词「Proxy MDN」搜索一下,就能得到一个详细的教程**。(在关键词后面加 MDN 是一个前端必备的小技巧哦)
假设我们有一个数据(对象)data,内容为
var data = { username: 'Hanbaoyi', age: 26}
现在我们给 data 创建一个代理 proxy
var proxy = new Proxy(data, {set: function(){...}, get: function(){...} })
那么,「proxy 就全权代理 data 了」,这话是什么意思呢?
意思就是 data 放假去了,如果你有任何事情要找 data,直接找 proxy 就好了,proxy 现在是 data 的秘书、代理人。
比如原本你如果要改 username,那么应该写 data.username = 'hanbaoyi';
那么现在你只需要写 proxy.username = 'hanbaoyi' 就好了。
原本你如果想写 console.log(data.username),现在也只需要 console.log(proxy.username) 就可以了。
这样做什么意义?
意义就是你能监控每一次对 data 的读写操作。
proxy.username = 'frank' 这句话实际上会运行 set 方法。set 方法可以对传入的值进行监控和过滤。
假设 PM 要求「username 前后不能含有空格」,用 Proxy 就很好实现这一需求,只需要把 set 写成这样:
set: function(obj, prop, value){
obj[prop] = value.trim()
}
再假设 PM 要求「统计 username 被读取的次数」,那么我们只需要把 get 写成这样:
get: function(obj, prop){
if(prop === 'username'){
count += 1
}
return obj[prop]
}
双向绑定
既然用 Proxy 能监控一个变量的读写情况,那么我们就很容易实现一个双向绑定了。
具体代码看这里。
以上,就是 Proxy 的简介了。
18、JSONP 是什么?
问题:JSONP 是什么?补充如下代码,实现一个JSONP方法。
function jsonp(setting){
//补充代码
}
jsonp({
url: 'http://photo.sina.cn/aj/index',
key: 'jsoncallback',
data: {
page: 1,
cate: 'recommend'
},
callback: function(ret){
console.log(ret)
}
})
背景:
小谷同学在学习 ajax 后想做一个简单的天气预报应用,但找不到合适的天气接口,便向小饥求助。应小谷的要求小饥写了一个获取当前访问者所在地天气的接口发布到线上,接口URL: http://api.jirengu.com/weather.php。 小谷把 URL复制到浏览器浏览器地址栏按下回车键,页面很神奇地展示了小谷所在城市的天气数据。于是小谷立即写了个页面,使用 ajax 调用当前接口,代码如下:
$.get('http://api.jirengu.com/weather.php')
.then(function(ret){
console.log(ret)
})
小谷打开控制台,满心期待想看到返回的天气数据,但映入眼帘的是几行红色的警告:
Paste_Image.png
「难道小饥在耍我?这人啊真不可信」,小谷愤愤的把报错截图甩给了小饥,正要开口责问,小饥说:「兄弟,这是跨域的问题,你用 JSONP 的方式调用吧,接口我都给你做了支持,直接使用 callback回调」 说完便埋入自己的代码里。
「啥 JSONP? callback? 跨域这个词貌似听过,我自己先查查别让小饥那小子鄙视我」小谷暗暗的想,「原来是跨域啊,你不早说,谢啦」,回到工位小谷赶紧打开谷歌。
同源策略(Same origin Policy)
浏览器出于安全方面的考虑,只允许与同域下的接口交互。
同域指的是?
- 同协议:如都是http或者https
- 同域名:如都是http://jirengu.com/a 和http://jirengu.com/b
- 同端口:如都是80端口
比如: 用户打开了 页面: http://jirengu.com/blog, 当前页面下的 js 向 http://jirengu.com/xxx的接口发 ajax 请求,浏览器是允许的。但假如向: http://hunger-valley.com/xxx 发ajax请求则会被浏览器阻止掉,因为存在跨域调用。
「原来如此,怪不得浏览器会报错。跨域不过如此嘛!那 JSONP是什么呢?」
HTML 中 script 标签可以加载其他域下的js,比如我们经常引入一个其他域下线上cdn的jQuery。那如何利用这个特性实现从其他域下获取数据呢?
可以先这样试试:
<script src="http://api.jirengu.com/weather.php"></script>
这时候会向天气接口发送请求获取数据,获取数据后做为 js 来执行。但这里有个问题, 数据是 JSON 格式的数据,直接作为 JS 运行的话我如何去得到这个数据来操作呢?
这样试试:
<script src="http://api.jirengu.com/weather.php?callback=showData"></script>
这个请求到达后端后,后端会去解析callback这个参数获取到字符串showData,在发送数据做如下处理:
之前后端返回数据: {"city": "hangzhou", "weather": "晴天"}
现在后端返回数据: showData({"city": "hangzhou", "weather": "晴天"})
前端script标签在加载数据后会把 「showData({"city": "hangzhou", "weather": "晴天"})」做为 js 来执行,这实际上就是调用showData这个函数,同时参数是 {"city": "hangzhou", "weather": "晴天"}。用户只需要在加载提前在页面定义好showData这个全局函数,在函数内部处理参数即可。
<script>
function showData(ret){ console.log(ret); }
</script>
<script src="http://api.jirengu.com/weather.php?callback=showData"></script>
「原来这就是 JSONP(JSON with padding),总结一下:」
- JSONP是通过 script 标签加载数据的方式去获取数据当做 JS 代码来执行
- 提前在页面上声明一个函数,函数名通过接口传参的方式传给后台,后台解析到函数名后在原始数据上「包裹」这个函数名,发送给前端。换句话说,JSONP 需要对应接口的后端的配合才能实现。
「原理很简单,但用起来代码好丑陋,我做个封装让小饥看看」
function jsonp(setting){
setting.data = setting.data || {}
setting.key = setting.key||'callback'
setting.callback = setting.callback||function(){}
setting.data[setting.key] = 'onGetData'
window.onGetData = function(data){
setting.callback (data);
}
var script = document.createElement('script')
var query = []
for(var key in setting.data){
query.push( key + '='+ encodeURIComponent(setting.data[key]) )
}
script.src = setting.url + '?' + query.join('&')
document.head.appendChild(script)
document.head.removeChild(script)
}
jsonp({
url: 'http://api.jirengu.com/weather.php',
callback: function(ret){
console.log(ret)
}
})
jsonp({
url: 'http://photo.sina.cn/aj/index',
key: 'jsoncallback',
data: {
page: 1, cate: 'recommend'
},
callback: function(ret){
console.log(ret)
}
})
19、JSON 是什么?
JSON 绝对不是对象,请问
- 1、JSON 是什么?
- 2、"null" 是 JSON 吗?
- 3、"1" 是 JSON 吗?
- 4、JSON 与 JS 对象的区别是什么?
JSON 是什么?
如果你在 Google 搜索 JSON,那么一眼就会看到 JSON 的官网http://json.org
官网会明明白白的告诉你,JSON 是一种数据格式。什么是格式?你可以理解为语法。JSON 的格式灵感来自于 JS 对象字面量的语法,但是两者没有任何关联。这种格式可以描述三种数据。
1. object(无序的「键-值」集合)。
语法如下:
下面三种写法都可以表示 object
{}
{"key1": "value1"} // string 对应 "key1",value 对应 "value1",后面会讲
{"key1": "value1", "key2": "value2"}
2. array(有序的值集合)
下面三种写法都可以表示 array
[]
[1]
[1,"hi"]
3. value
value 对应对象语法图里的 value 和数组语法图里 value,value 也可以是 object 或 array,所以下面的语法成立:
{"key1": { "key2" : "value2" } }
[ 1, [ 2, 3 ] ]
另外值还可以是 string、number、true、false 和 null。
string 的语法如下:
你可能奇怪为什么 string 的语法这么复杂,我举例来说明你就明白了:
"你好"
""你好""
"\你好\"
"/你好/"
"\b\f\n\r\t特殊符号"
"\u4f60用编码表示字符"
上面都是合法的 string。这也是「JSON 中字符串必须使用双引号」的原因——规定如此。
number 的语法如下,有兴趣可以自己走一遍:
另外需要特殊提醒一下,true、false 和 null 都是合法的 JSON。
JSON 和 JS Object 的区别
简单来说,两种没有任何关联。
JSON 语法的作者是道格拉斯(Douglas Crockford),JS 语法的作者是布兰登・艾奇(Brendan Eich)。道格拉斯发明 JSON 的时候参考了 JS 的对象语法,仅此而已。
如果硬要说区别:
- JSON 的字符串必须用双引号。
- JSON 无法表示 undefined,只能表示 "undefined"
- JSON 无法表示函数
- JSON 的对象语法不能有引用
20、JS 中的 Symbol 是什么?
ES 6 引入了一个新的数据类型 Symbol,它是用来做什么的呢?
为了说明 Symbol 的作用,我们先来描述一个使用场景。
我们在做一个游戏程序,用户需要选择角色的种族。
var race = {
protoss: 'protoss', // 神族
terran: 'terran', // 人族
zerg: 'zerg' // 虫族
}
function createRole(type){
if(type === race.protoss){创建神族角色}
else if(type === race.terran){创建人族角色}
else if(type === race.zerg){创建虫族角色}
}
那么用户选择种族后,就需要调用 createRole 来创建角色:
// 传入字符串
createRole('zerg')
// 或者传入变量
createRole(race.zerg)
如果使用 createRole(race.zerg),那么聪明的读者会发现一个问题:race.protoss、race.terran、race.zerg 的值为多少并不重要。
改为如下写法,对 createRole(race.zerg) 毫无影响:
var race = {
protoss: 'askdjaslkfjas;lfkjas;flkj', // 神族
terran: ';lkfalksjfl;askjfsfal;skfj', // 人族
zerg: 'qwieqwoirqwoiruoiwqoisrqwroiu' // 虫族
}
也就是说:
race.zerg 的值是多少无所谓,只要它的值跟 race.protoss 和 race.terran 的值不一样就行。
Symbol 的用途就是如此:Symbol 可以创建一个独一无二的值(但并不是字符串)。
用 Symbol 来改写上面的 race:
Paste_Image.pngvar race = {
protoss: Symbol(),
terran: Symbol(),
zerg: Symbol()
}
race.protoss !== race.terran // true
race.protoss !== race.zerg // true
你也可以给每个 Symbol 起一个名字:
var race = {
protoss: Symbol('protoss'),
terran: Symbol('terran'),
zerg: Symbol('zerg')
}
不过这个名字跟 Symbol 的值并没有关系,你可以认为这个名字就是个注释。如下代码可以证明 Symbol 的名字与值无关:
var a1 = Symbol('a')
var a2 = Symbol('a')
a1 !== a2 // true