全局作用域和函数作用域
var a = 1;
function changeA() {
console.log(a);
var a;
}
changeA();
猜猜输出啥,undefined,因为函数作用域内发生了变量提升,函数内的代码相当于
var a; console.log(a);
var a = 1;
function changeA() {
a = 2;
console.log(a);
var a;
}
changeA();
猜猜输出啥,a=2,因为变量提升是直接到函数作用域顶部,在输出a之前,a已经赋值2,且函数作用域内a是局部变量
var a = 1;
function changeA(a) {
console.log(a);
var a;
}
changeA(a);
输出 1,因为a是传参进来的,相当于在函数作用域内a已经声明和赋值了 另外:
(function () {
a=5;
console.log(window.a) // undefined,发生了变量提升
var a = 10;
})() // 这个自执行保证了 `var a` 发生变量提升时,不会污染到全局window.a
函数的声明优先级低于变量:
var sum = function () {
console.log(1);
}
function sum() {
console.log(2);
}
sum() // 1
变量提升
函数声明提升会被后面的覆盖
foo(); // 3
function foo() {
console.log( 1 );
}
function foo() {
console.log( 3 );
}
if-else语句里面的函数声明都有效
foo(); // "b"
var a = true;
if (a) {
function foo() { console.log("a"); }
} else {
function foo() { console.log("b"); }
}
引用传递和值传递
函数调用中,传递是一个数值,我们称为 “值传递”。 函数调用中,传递是对象,一般称为 “引用传递”。引用类型:Object Array Fucntion;对于传参而言,传入的东西是不变的。
var a = 1;
function changeA(a) {
a++;
}
changeA(a);
console.log(a);
这里会输出1,a的值没有改变
var a = [1, 2, 3];
function changeA(a) {
a[0] = 2;
}
changeA(a);
console.log(a);
这里输出[2, 2, 3],传入的是指针,指针没有改变,而值发生了变化
再看看下面的例子,如果数组以...arr的方式传递,会发生什么
var a = [1, 2, 3];
function changeA(...a) {
a[0] = 2;
}
changeA(a);
console.log(a); // [1, 2, 3]
严格相等(===)的特殊值
引用类型全等于永远返回false,因为变量存储的是地址值,比如new Object
NaN === NaN // false
+0 === -0 // true
+0 === 0 // true
-0 === 0 // true
+Infinity === Infinity // true
+Infinity === -Infinity // false
-Infinity === Infinity // false
null === undefined // false
[] === [] // false
{} === {} // false
隐式类型转换
Javascript是弱类型语言,之所以不同的数据类型之间可以做运算,是因为JavaScript引擎在运算之前会悄悄的把他们进行了隐式类型转换的。
以下假设为比较 x == y的情况,Type(x)指的是x的数据类型,Type(y)指的是y的类型,最终返回值只有true或false。
- Type(x)与Type(y)相同时,进行严格相等比较
- x是undefined,而y是null时,返回true
- x是null,而y是undefined时,返回true
- Type(x)是Number而Type(y)是String时,进行x == ToNumber(y)比较
- Type(x)是String而Type(y)是Number时,进行ToNumber(x) == y比较
- Type(x)是Boolean时,进行ToNumber(x) == y
- Type(y)是Boolean时,进行x == ToNumber(y)
- Type(x)是Number或String其中一种,而Type(y)是个Object时,进行x == ToPrimitive(y)比较
- Type(x)是个Object,而Type(y)是Number或String其中一种时,进行ToPrimitive(x) == y比较
- 其他情况,返回false
参考教程
其实第一次读的时候,还是有点理解不了。还是系统的剖析一下吧,简单点来说,比较x == y
这个表达式,当 x 和 y 相同,直接进行值比较。当 x 和 y 不同,最终一定会把 x 和 y 转换为要么number、要么string再做值比较。
当 x 和 y 都是 基础类型 时,会转化为相同的基础类型再比较,boolean类型会转化为Number类型再做比较,当有一方是Number时,最终会转化为Number类型再比较,Number类型的优先级是最高的,所以需要熟悉一下Number(x)
这个函数,Number(x)
和+x
效果是一样的,枚举一下可能遇见的情况吧:
Number('1.5') //1.5
Number('1,2') //NaN
Number({}) //NaN
Number([]) //0
Number([2]) //2
Number(true) //1
Number(null) //0
Number(undefined) //NaN
当 x 和 y 两个都是引用类型,比如数组、对象、Function,会直接返回false,因为 x 和 y 储存的是地址。
当 x 和 y 一方是引用类型时,会进行 ToPrimitive(x || y)
的转化。如果 x.valueOf
能返回基础数据类型,则会优先调用 valueOf
方法,如果不能返回基础数据类型,则会调用 toString
方法。toString
一定会返回基础数据类型。
找几个例子说说:
true == true
Number(true) == true
=>1 == Number(true)
=>1 == 1
左值true
先转化为数字,右值true
再转化为数字,然后进行值比较
答案:true
true == '123'
Number(true) == '123'
=>1 == Number('123')
=>1 == 123
true先转化为数字,再把'123'转化为数字123,再比较左值和右值
答案:false
'123' == 2
Number('123') == 2
=>123 == 2
'123'先转化为数字,再比较左值和右值
答案:false
[1,2] == 2
toToPrimitive([1,2]) == 2
=>'1,2' == 2
=>Number('1,2') == 2
=>NaN == 2
只解析一项,NaN也是Number类型的一种,NaN与任何数字比较,返回false
答案:false
数组类型
[1,2,3].valueOf() //[1,2,3] 不是基础数据类型
[1,2,3].toString() //1,2,3,数组类型toPrimitive会调用这个方法
对象类型
var c = {a:1}; //必须赋值,{a:1}.valueOf()会报错
c.valueOf() //{a:1}不是基础数据类型
c.toString() //[object Object],toPrimitive会调用这个方法
c == '[object Object]' //true
解释一下为啥 c == '[object Object]'
返回true
toPrimitive(c) == '[object Object]'
=>'[object Object]' == '[object Object]'
函数类型
function b() { return 2;}
b.valueOf() //返回函数本身,即function b() { return 2;}
b.toString() //返回字符串,即'function b() { return 2;}',toPrimitive采用这个
b == 'function b() { return 2;}' //true
状态码
不介绍常见的状态码,主要针对1XX到5XX能加分的详细说明
100 - Continue 初始的请求已经接受,客户应当继续发送请求的其余部分
- 用于客户端在发送 post 数据给服务器时,看服务器是否处理 post 的数据,如果不处理,客户端则不上传 post 是数据,反之则上传。在实际应用中,通过 post 上传大数据时,才会使用到 100-continue 协议
- 如果客户端有 post 数据要上传,可以考虑使用 100-continue 协议。在请求头中加入 {“Expect”:”100-continue”}
- 如果在发送 100-continue 前收到了 post 数据(客户端提前发送 post 数据),则不发送 100 响应码
101 - 服务器已经理解了客户端的请求,并将通过Upgrade消息头通知客户端采用不同的协议来完成这个请求
- 比如请求使用了webSocket协议
202 - 接受和处理、但处理未完成
204 - 服务器成功处理了请求,但没有返回任何内容
206 - 服务器已经完成了部分用户的GET请求
- 在请求mp4文件的时候会返回这个,原因是这个东西会一部分一部分返回
303 - 临时重定向,和302状态码有着相同的功能,但是303明确表示客户端应当采用get方式请求资源
307 - 临时重定向,和302状态码有着相同的功能,当301、302、303响应状态码返回时,几乎所有浏览器都会把post改成get,并删除请求报文内的主体,之后请求会自动再次发送。307会遵照浏览器标准,不会从post变为get。但是对于处理响应时的行为,各种浏览器有可能出现不同的情况。
400 - 语义有误,当前请求无法被服务器理解。除非进行修改,否则客户端不应该重复提交这个请求
- 比如应该用https协议
- 比如上传的json数据语法错误
401 - 当前请求需要用户验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息,浏览器据此显示用户名字/密码对话框,然后在填写合适的Authorization头后再次发出请求
403 - Forbidden,服务器已经理解请求,但是拒绝执行它
405 - 请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个Allow 头信息用以表示出当前资源能够接受的请求方法的列表
410 - Gone,被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的状况应当被认为是永久性的
414 - Request-URI Too Long,请求的URI 长度超过了服务器能够解释的长度,因此服务器拒绝对该请求提供服务
415 - Unsupported Media Type,对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝
421 - too many connections,从当前客户端所在的IP地址到服务器的连接数超过了服务器许可的最大范围。通常,这里的IP地址指的是从服务器上看到的客户端地址(比如用户的网关或者代理服务器地址)
500 - Internal Server Error,作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
502 - Bad Gateway,作为网关或者代理工作的服务器尝试执行请求时,从上游服务器接收到无效的响应
503 - Service Unavailable,由于临时的服务器维护或者过载,服务器当前无法处理请求
504 - Gateway Timeout,作为网关或者代理工作的服务器尝试执行请求时,未能及时从上游服务器(URI标识出的服务器,例如HTTP、FTP、LDAP)或者辅助服务器(例如DNS)收到响应
DNS解析
一个域名的DNS记录会在本地有两种缓存:浏览器缓存和操作系统(OS)缓存,会优先访问浏览器缓存,如果未命中则访问OS缓存,最后再访问DNS服务器(一般是ISP提供),然后DNS服务器会递归式的查找域名记录,然后返回。
DNS记录会有一个ttl值(time to live),单位是秒,意思是这个记录最大有效期是多少。经过实验,OS缓存会参考ttl值,但是不完全等于ttl值,而浏览器DNS缓存的时间跟ttl值无关,每种浏览器都使用一个固定值。
修改hosts文件之后,为啥有时会立刻生效,有时却一直不生效呢?在修改hosts文件后,所有OS中DNS缓存会被清空,而浏览器缓存则不发生变化,在 chrome://net-internals/#dns
中点击 Clear Host Cache
会清空OS缓存。如果发现DNS更改不成功,可以静待几十秒。
查找浏览器缓存的DNS服务器
浏览器在获取网站域名的实际IP地址后会对其IP进行缓存,减少网络请求的损耗。每种浏览器都有一个固定的DNS缓存时间,其中Chrome的过期时间是1分钟,在这个期限内不会重新请求DNS。chrome://net-internals/#dns
OS DNS缓存
OS缓存会参考DNS服务器响应的TTL值,但是不完全等于TTL值
ISP DNS服务器
先检查一下自己的缓存中有没有这个地址,有的话就直接返回,没有的话就去根域找,从根域开始递归查询IP
根域递归
根域的地址是写死在ISP DNS服务器上的,根域即是/;比如www.a.com这样的域名,先去根域找.com的服务器对应IP,然后.com的服务器对应IP找到a.com的服务器IP...
正则
先通关正则,打好基础,否则会看不明白,戳我。在正则优化的情况下,使用new RegExp会比较快。原因是new RegExp会在代码执行的过程中编译正则,编译就是在内存中开辟一个空间存放变量或函数,字面量有个废弃的compile方法也可以做到这个事情。
腾讯QQ
腾讯QQ号从10000开始,最少5位
[1-9][0-9]{4,}
匹配前后空格
\s表示匹配一个空白符,包括空格、制表符、换页符、换行符和其他 Unicode 空格,具体点就是[ \f\n\r\t\v\u00a0\u1680\u180e\u2000\u2001\u2002\u2003\u2004 \u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f \u3000
/(^\s+)|(\s+$)/g
数字分隔
将阿拉伯数字每三位一逗号分隔,如:15000000转化为15,000,000
'1500000000000'.replace(/\B(?=(\d{3})+$)/g,',')
匹配中文字符
[\u4e00-\u9fa5]
匹配一个域名
DNS规定,域名中的标号都由英文字母和数字组成,每一个标号不超过63个字符,也不区分大小写字母。标号中除连字符(-)外不能使用其他的标点符号。级别最低的域名写在最左边,而级别最高的域名写在最右边。由多个标号组成的完整域名总共不超过255个字符。
/[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/
查找英文句子中所有的单词
分析:第一,不区分大小写;第二,带'的也算在里面,比如don't;第三,带-的也算在里面,比如x-ray
var str = "When you are old and grey and full of sleep, don't make me think. And hid his face amid a crowd of stars.";
str.match(/[a-z]+([-'][a-z]+)?/ig);
获取url上某个参数名为name的正则
分析:参数名=值,不同参数间用&分隔开
var name= 'rqlang';
reg = new RegExp('[&?]'+name+'=([^& ]+)')
str = 'https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&tn=baidu&wd=%E6%AD%A3%E5%88%99%E8%8E%B7%E5%8F%96%E6%9F%90%E4%B8%AA%E5%8F%82%E6%95%B0&oq=%25E6%25AD%25A3%25E5%2588%2599%25E8%258E%25B7%25E5%258F%2596%25E6%259F%2590%25E4%25B8%25AA%25E5%258F%2582%25E6%2595%25B0&rsv_pq=baac736e00031a97&rsv_t=515dwC%2Bf5ZRACPl1sr7KbYkxhUwe9G1VxfTPLWRBaQ9vh4Oa8jc6nfh0pQg&rqlang=cn&rsv_enter=1&inputT=2550&rsv_sug3=107&rsv_sug1=103&rsv_sug7=100&rsv_sug2=0&rsv_sug4=3469'
val = str.match(reg)[1];
升级一下,获取url中所有的参数名
var propReg = /([?&][^=]+)/g,
str = location.search,
arr = str.match(propReg);
arr.forEach(function(val, index, arr) {
arr[index] = arr[index].replace(/^(&|\?)/, '');
});
console.log(arr); // 所有的参数名
再升级一下,获取url中所有参数对键值,以对象的方式展现
var reg = /([&\?][^=]+)=([^& ]+)/g,
str = location.search,
arr = str.match(reg);
arr.reduce(function(obj, val, index, arr) {
var reg = /[&?]([^=]+)=([^& ]+)/,
tmpArr = val.match(reg);
obj[tmpArr[1]] = tmpArr[2];
return obj;
}, {});
常用的函数
获取cookie值
cookie的特征,以';'为分隔,末字段也许不带';',prop值前面匹配一个或0个空格
function getCookie(name) {
var arr,reg=new RegExp("(^| )"+name+"=([^;]*)(;|$)");
if(arr=document.cookie.match(reg))
return unescape(arr[2]);
else
return null;
}
手写一个ajax请求
readyState各种值,记不住没关系,记住这几个状态都是围绕着XMLHttpRequest对象本身,且最后一个状态是4就可以了:
- 0,未初始化,(XMLHttpRequest)对象已经创建,但还没有调用open()方法
- 1,载入/正在发送请求,即调用open方法
- 2,载入完成/数据接收,即send()方法执行完成
- 3,loading,交互/解析数据,此阶段解析接收到的服务器端响应数据
- 4,响应内容解析完成,可以在客户端调用了
var xmlHttp = new XMLHttpRequest();
xmlHttp.onreadystatechange = function () {
// readyState
if (this.readyState == 4 && this.status == 200) {
console.log(1);
}
}
xmlHttp.open('GET', 'https://www.afea.com', false);
xmlHttp.setRequestHeader('X-Requested-With');
xmlHttp.withCredentials = true; // 跨域请求携带cookie
xmlHttp.send(null);
简单实现双向数据绑定
var data = {};
object.defineProperty(data, 'text', {
set(value) {
$input.value = value;
this.value = value;
}
});
$input.onChange = function () {
data.text = e.target.value;
}
实现一个累加器
形如sum(1,2)(3)(4,5)
的样子:
sum(1,2) // 返回3
sum(3)(1,2) // 返回6
sum(1,3)(5)(2) // 返回11
思路是利用闭包,改写toString方法。
function add () {
var total = 0;
var args1 = [...arguments];
var sum = function (...args) {
total = args1.concat(args).reduce((total, a) => total += a, total)
return sum;
}
sum.toString = function() { // sum方法调用,return必然会调用toString方法
return total;
}
return sum;
}
重写bind功能
bind原是Function.prototype上的方法,可以修改函数的this指向
Function.prototype.bind = function (context) {
// this指向调用函数
if (typeof this !== 'function') {
throw new TypeError('not a function');
}
var args = Array.prototype.slice.call(arguments, 1);
return function () {
return this.apply(context, args.concat(Array.prototype.slice.call(arguments)))
}
}
实现一个对象或者数组的深拷贝
浅拷贝只复制指向某个对象的引用地址,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。浅复制只复制一层对象的属性,而深复制则递归复制了所有层级。
function deepClone(target) {
if (typeof target !== 'object' ) return target;
var arr_tmp = target instanceof Array ? [] : {};
for (var i in target) {
arr_tmp[i] = typeof target[i] !== 'object' ? target[i] : deepClone(target[i]);
}
return arr_tmp;
}
实现数组的扁平化
递归实现,如果当前元素是数组,则继续递归,否则加入最终返回的扁平化数组
const flattern = (arr) => arr.reduce((a, item) => Array.isArray(item) ? a.concat(flattern(item)) : a.concat(item), [])
循环利用队列
const flattern = (arr) => {
var finalArray = [];
if (!Array.isArray(arr)) return;
while(arr.length) {
var target = arr.shift();
if (Array.isArray(target)) {
arr = target.concat(arr);
} else {
finalArray.push(target)
}
}
return finalArray;
}
防抖(debounce)
防抖主要是为了限制函数的执行频次,以优化函数执行频次过高导致的响应速度跟不上触发频率的问题。如果倒计时没有结束,则清空倒计时,再重新计时。有个弊端,如果事件不断循环触发,并且小于等待时间,则不可能执行回调事件,所以后来又催生了节流。
function debounce(fn, wait, immediate) {
var timer;
return function() {
var that = this, args = arguments;
if (immediate) {
fn.apply(that, args);
immediate = false;
}
clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(that, args);
}, wait);
}
}
节流(Throttle)
节流主要是为了限制函数的执行频次,以优化函数执行频次过高导致的响应速度跟不上触发频率的问题。要记住这个概念,可以联想水龙头滴水,水的一定量的,会一滴一滴的流出去,但是不一定会流光,时间间隔是一定的。直接上代码:
function throttle(fn, wait, immediate) {
var timer, previous = new Date().getTime();
return function() {
var that = this, args = arguments, now = new Date().getTime();
if (immediate) {
fn.apply(that, args);
immediate = false;
}
if (wait >= now-previous) { // 到一定时间一定执行一次
previous = now;
fn.apply(that, args);
clearTimeout(timer);
} else {
timer = setTimeout(function() {
previous = new Date().getTime();
fn.apply(that, args);
}, wait);
}
}
}
异步调用链
要求:写一个对象,实现调用a.work().sleep(20).lunch()
,会输出
work
sleep 20 seconds
(wait 20 seconds)
lunch
用队列实现链式调用,当调用链很长的时候递归,其实是会报栈溢出的,最好在run里加个setTimeout 0
let a = {
query: [],
status: false,
run: function () { // 关键点,递归执行函数
setTimeout(() => {
if (this.status) return; // 如果队列还在运行中,则返回
if (this.query.length > 0) {
this.query.shift()(this.run.bind(this))
}
}, 0)
},
work: function () {
this.query.push((fn) => {
console.log('work');
fn();
});
this.run();
return this;
},
lunch: function () {
this.query.push((fn) => {
console.log('lunch');
fn();
});
this.run();
return this;
},
sleep: function (time) {
this.query.push((fn) => {
this.status = true; // 只有异步会阻塞队列执行而已,所以status的更新放在这里
console.log(`sleep ${time} seconds`);
setTimeout(() => {
this.status = false;
fn();
}, time * 1000);
});
this.run();
return this;
}
}
数据结构和算法
二分查找
假设有一个有序数组,需要查找一个数值为3的元素,如果存在,返回第一个元素的下标,否则返回-1。
function binarySearch(arr, target) {
var low = 0, high = arr.length-1, mid;
while(low < high) {
mid = ~~((low + high)/2);
if(arr[mid] < target) {
low = mid+1;
} else {
high = mid;
}
}
if (arr[low] === target) return low;
else return -1;
}
插入排序
类似整理扑克牌,将每一张牌插到其他已经有序的牌中适当的位置。冒泡和选择就不用说了,一个正方向一个反方向,两个for循环搞定的。
for (var i = 1; i < arr.length; i++) {
for(var j=i;j > 0 && arr[j] < arr[j-1]; j--;) {
[arr[j], arr[j-1]] = [arr[j-1], arr[j]]
}
}
希尔排序
原理戳这,插入排序的晋级版,以gap为界限分为一组,每一组进行插入排序计算,一开始时,一般来说gap=length/2,所以稳定的复杂度为nlogn
function heerSort(arr) {
var gap = ~~(arr.length/2); // 取整竟然比/优先级高,只能用括号补了
for (var i=gap;i>0;i=~~(i/2))
for (var j=i;j<arr.length;j+=i)
for(var k=j;k>=i&&arr[k]<arr[k-i];k-=i)
[arr[k],arr[k-i]]=[arr[k-i],arr[k]];
}
归并排序
原理戳这,是利用归并的思想实现的排序方法,该算法采用经典的分治(divide-and-conquer)策略(分治法将问题分(divide)成一些小的问题然后递归求解,而治(conquer)的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
function mergeSort(arr, low, high, temp) {
if (low < high) {
var mid = parseInt((low+high)/2);
mergeSort(arr, low, mid, temp);
mergeSort(arr, mid+1, high, temp);
merge(arr, low, high, temp);
}
}
function merge(arr, low, high, temp) {
var mid = parseInt((low+high)/2),
i = low,
j = mid+1,
k = 0;
while (i<=mid&&j<=high) {
if (a[i] < a[j]) {
temp[k++] = a[i++];
} else {
temp[k++] = a[j++];
}
}
while(i<=mid) {
temp[k++] = a[i++];
}
while(j<=high) {
temp[k++] = a[j++]
}
for (var i=0;i<k;i++) {
a[low+i] = temp[i];
}
}
快速排序
简单点来说,就是以一个数为基准(通常是最左边的数),把这个序列小于这个数的数放在这个数的左边,如果大于这个数,则放在右边。平均性能O(nlogn),最坏性能是O(n2)相当于插入排序,在正序和逆序的时候出现,递归划分为有一边为0个
function quickSort(arr, low, high) {
var i = low,
j = high,
temp = arr[i];
if(i>=j) return;
while(i<j) {
while(i<j&&arr[j]>=temp)
j--;
if(i<j)
arr[i]=arr[j];
while(i<j&&arr[i]<=temp)
i++;
if(i<j)
arr[j]=arr[i];
}
arr[i]=temp;
quickSort(arr, low,i-1);
quickSort(arr, i+1, high);
}