文末有全部内容的下载链接
等于操作符用两个等于号( == )表示,如果操作数相等,则会返回 true
前面文章,我们提到在JavaScript
中存在隐式转换。等于操作符(==)在比较中会先进行类型转换,再确定操作数是否相等
遵循以下规则:
如果任一操作数是布尔值,则将其转换为数值再比较是否相等
let result1 = (true == 1); // true
如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等
let result1 = ("55" == 55); // true
如果一个操作数是对象,另一个操作数不是,则调用对象的 valueOf()
方法取得其原始值,再根据前面的规则进行比较
let obj = {valueOf:function(){return 1}}
let result1 = (obj == 1); // true
null
和undefined
相等
let result1 = (null == undefined ); // true
如果有任一操作数是 NaN
,则相等操作符返回 false
let result1 = (NaN == NaN ); // false
如果两个操作数都是对象,则比较它们是不是同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回true
let obj1 = {name:"xxx"}
let obj2 = {name:"xxx"}
let result1 = (obj1 == obj2 ); // false
下面进一步做个小结:
两个都为简单类型,字符串和布尔值都会转换成数值,再比较
简单类型与引用类型比较,对象转化成其原始类型的值,再比较
两个都为引用类型,则比较它们是否指向同一个对象
null 和 undefined 相等
存在 NaN 则返回 false
全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true
。即类型相同,值也需相同
let result1 = ("55" === 55); // false,不相等,因为数据类型不同
let result2 = (55 === 55); // true,相等,因为数据类型相同值也相同
undefined
和 null
与自身严格相等
let result1 = (null === null) //true
let result2 = (undefined === undefined) //true
相等操作符(==)会做类型转换,再进行值的比较,全等运算符不会做类型转换
let result1 = ("55" === 55); // false,不相等,因为数据类型不同
let result2 = (55 === 55); // true,相等,因为数据类型相同值也相同
null
和 undefined
比较,相等操作符(==)为true
,全等为false
let result1 = (null == undefined ); // true
let result2 = (null === undefined); // false
相等运算符隐藏的类型转换,会带来一些违反直觉的结果
'' == '0' // false
0 == '' // true
0 == '0' // true
false == 'false' // false
false == '0' // true
false == undefined // false
false == null // false
null == undefined // true
' \t\r\n' == 0 // true
但在比较null
的情况的时候,我们一般使用相等操作符==
const obj = {};
if(obj.x == null){
console.log("1"); //执行
}
等同于下面写法
if(obj.x === null || obj.x === undefined) {
...
}
使用相等操作符(==)的写法明显更加简洁了
所以,除了在比较对象属性为null
或者undefined
的情况下,我们可以使用相等操作符(),其他情况建议一律使用全等操作符(=)
AJAX
全称(Async Javascript and XML)
即异步的 JavaScript
和 XML
,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页
Ajax
的原理简单来说通过XmlHttpRequest
对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript
来操作DOM
而更新页面
流程图如下:
下面举个例子:
领导想找小李汇报一下工作,就委托秘书去叫小李,自己就接着做其他事情,直到秘书告诉他小李已经到了,最后小李跟领导汇报工作
Ajax
请求数据流程与“领导想找小李汇报一下工作”类似,上述秘书就相当于XMLHttpRequest
对象,领导相当于浏览器,响应数据相当于小李
浏览器可以发送HTTP
请求后,接着做其他事情,等收到XHR
返回来的数据再进行操作
实现 Ajax
异步交互需要服务器逻辑进行配合,需要完成以下步骤:
创建 Ajax
的核心对象 XMLHttpRequest
对象
通过 XMLHttpRequest
对象的 open()
方法与服务端建立连接
构建请求所需的数据内容,并通过 XMLHttpRequest
对象的 send()
方法发送给服务器端
通过 XMLHttpRequest
对象提供的 onreadystatechange
事件监听服务器端你的通信状态
接受并处理服务端向客户端响应的数据结果
将处理结果更新到 HTML
页面中
通过XMLHttpRequest()
构造函数用于初始化一个 XMLHttpRequest
实例对象
const xhr = new XMLHttpRequest();
通过 XMLHttpRequest
对象的 open()
方法与服务器建立连接
xhr.open(method, url, [async][, user][, password])
参数说明:
method
:表示当前的请求方式,常见的有GET
、POST
url
:服务端地址
async
:布尔值,表示是否异步执行操作,默认为true
user
: 可选的用户名用于认证用途;默认为`null
password
: 可选的密码用于认证用途,默认为`null
通过 XMLHttpRequest
对象的 send()
方法,将客户端页面的数据发送给服务端
xhr.send([body])
body
: 在 XHR
请求中要发送的数据体,如果不传递数据则为 null
如果使用GET
请求发送数据的时候,需要注意如下:
open()
方法中的url
地址中send()
方法中参数设置为null
onreadystatechange
事件用于监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState
,
关于XMLHttpRequest.readyState
属性有五个状态,如下图显示
只要 readyState
属性值一变化,就会触发一次 readystatechange
事件
XMLHttpRequest.responseText
属性用于接收服务器端的响应结果
举个例子:
const request = new XMLHttpRequest()
request.onreadystatechange = function(e){
if(request.readyState === 4){ // 整个请求过程完毕
if(request.status >= 200 && request.status <= 300){
console.log(request.responseText) // 服务端返回的结果
}else if(request.status >=400){
console.log("错误信息:" + request.status)
}
}
}
request.open('POST','http://xxxx')
request.send()
通过上面对XMLHttpRequest
对象的了解,下面来封装一个简单的ajax
请求
//封装一个ajax请求
function ajax(options) {
//创建XMLHttpRequest对象
const xhr = new XMLHttpRequest()
//初始化参数的内容
options = options || {}
options.type = (options.type || 'GET').toUpperCase()
options.dataType = options.dataType || 'json'
const params = options.data
//发送请求
if (options.type === 'GET') {
xhr.open('GET', options.url + '?' + params, true)
xhr.send(null)
} else if (options.type === 'POST') {
xhr.open('POST', options.url, true)
xhr.send(params)
//接收请求
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
let status = xhr.status
if (status >= 200 && status < 300) {
options.success && options.success(xhr.responseText, xhr.responseXML)
} else {
options.fail && options.fail(status)
}
}
}
}
使用方式如下
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(text,xml){//请求成功后的回调函数
console.log(text)
},
fail: function(status){请求失败后的回调函数
console.log(status)
}
})
数组基本操作可以归纳为 增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不会
下面对数组常用的操作方法做一个归纳
下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响
push()
方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长度
let colors = []; // 创建一个数组
let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2
unshift()在数组开头添加任意多个值,然后返回新的数组长度
let colors = new Array(); // 创建一个数组
let count = colors.unshift("red", "green"); // 从数组开头推入两项
alert(count); // 2
传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []
首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组
let colors = ["red", "green", "blue"];
let colors2 = colors.concat("yellow", ["black", "brown"]);
console.log(colors); // ["red", "green","blue"]
console.log(colors2); // ["red", "green", "blue", "yellow", "black", "brown"]
下面三种都会影响原数组,最后一项不影响原数组:
pop()
方法用于删除数组的最后一项,同时减少数组的 length
值,返回被删除的项
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一项
console.log(item) // green
console.log(colors.length) // 1
shift()
方法用于删除数组的第一项,同时减少数组的 length
值,返回被删除的项
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一项
console.log(item) // red
console.log(colors.length) // 1
传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一项
console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组
slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors) // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow
即修改原来数组的内容,常用splice
传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组
即查找元素,返回元素坐标或者元素值
返回要查找的元素在数组中的位置,如果没找到则返回 -1
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3
返回要查找的元素在数组中的位置,找到返回true
,否则false
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true
返回第一个匹配的元素
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}
数组有两个方法可以用来对元素重新排序:
顾名思义,将数组元素方向反转
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1
sort()方法接受一个比较函数,用于判断哪个值应该排在前面
function compare(value1, value2) {
if (value1 < value2) {
return -1;
} else if (value1 > value2) {
return 1;
} else {
return 0;
}
}
let values = [0, 1, 5, 10, 15];
values.sort(compare);
alert(values); // 0,1,5,10,15
常见的转换方法有:
join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue
常用来迭代数组的方法(都不改变原数组)有如下:
对数组每一项都运行传入的测试函数,如果至少有1个元素返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true
对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false
对数组每一项都运行传入的函数,没有返回值
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});
对数组每一项都运行传入的函数,函数返回 true
的项会组成数组之后返回
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let filterResult = numbers.filter((item, index, array) => item > 2);
console.log(filterResult); // 3,4,5,4,3
对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let mapResult = numbers.map((item, index, array) => item * 2);
console.log(mapResult) // 2,4,6,8,10,8,6,4,2
call
、apply
、bind
作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this
指向
那么什么情况下需要改变this
的指向呢?下面举个例子
var name = "lucy";
var obj = {
name: "martin",
say: function () {
console.log(this.name);
}
};
obj.say(); // martin,this 指向 obj 对象
setTimeout(obj.say,0); // lucy,this 指向 window 对象
从上面可以看到,正常情况say
方法输出martin
但是我们把say
放在setTimeout
方法中,在定时器中是作为回调函数来执行的,因此回到主栈执行时是在全局执行上下文的环境中执行的,这时候this
指向window
,所以输出lucy
我们实际需要的是this
指向obj
对象,这时候就需要该改变this
指向了
setTimeout(obj.say.bind(obj),0); //martin,this指向obj对象
下面再来看看apply
、call
、bind
的使用
apply
接受两个参数,第一个参数是this
的指向,第二个参数是函数接受的参数,以数组的形式传入
改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次
function fn(...args){
console.log(this,args);
}
let obj = {
myname:"张三"
}
fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window
当第一个参数为null
、undefined
的时候,默认指向window
(在浏览器中)
fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window
call
方法的第一个参数也是this
的指向,后面传入的是一个参数列表
跟apply
一样,改变this
指向后原函数会立即执行,且此方法只是临时改变this
指向一次
function fn(...args){
console.log(this,args);
}
let obj = {
myname:"张三"
}
fn.call(obj,1,2); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window
同样的,当第一个参数为null
、undefined
的时候,默认指向window
(在浏览器中)
fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window
bind方法和call很相似,第一参数也是this
的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
改变this
指向后不会立即执行,而是返回一个永久改变this
指向的函数
function fn(...args){
console.log(this,args);
}
let obj = {
myname:"张三"
}
const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj
fn(1,2) // this指向window
从上面可以看到,apply
、call
、bind
三者的区别在于:
this
对象指向this
要指向的对象,如果如果没有这个参数或参数为undefined
或null
,则默认指向全局window
apply
是数组,而call
是参数列表,且apply
和call
是一次性传入参数,而bind
可以分为多次传入bind
是返回绑定this之后的函数,apply
、call
则是立即执行实现bind
的步骤,我们可以分解成为三部分:
this
指向// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()
// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)
new
关键字整体实现代码如下:
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new TypeError("Error");
}
// 获取参数
const args = [...arguments].slice(1),
fn = this;
return function Fn() {
// 根据调用方式,传入不同绑定值
return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments));
}
}
BOM
(Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象
其作用就是跟浏览器做一些交互效果,比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率
浏览器的全部内容可以看成DOM
,整个浏览器可以看成BOM
。区别如下:
Bom
的核心对象是window
,它表示浏览器的一个实例
在浏览器中,window
对象有双重角色,即是浏览器窗口的一个接口,又是全局对象
因此所有在全局作用域中声明的变量、函数都会变成window
对象的属性和方法
var name = 'js每日一题';
function lookName(){
alert(this.name);
}
console.log(window.name); //js每日一题
lookName(); //js每日一题
window.lookName(); //js每日一题
关于窗口控制方法如下:
moveBy(x,y)
:从当前位置水平移动窗体x个像素,垂直移动窗体y个像素,x为负数,将向左移动窗体,y为负数,将向上移动窗体moveTo(x,y)
:移动窗体左上角到相对于屏幕左上角的(x,y)点resizeBy(w,h)
:相对窗体当前的大小,宽度调整w个像素,高度调整h个像素。如果参数为负值,将缩小窗体,反之扩大窗体resizeTo(w,h)
:把窗体宽度调整为w个像素,高度调整为h个像素scrollTo(x,y)
:如果有滚动条,将横向滚动条移动到相对于窗体宽度为x个像素的位置,将纵向滚动条移动到相对于窗体高度为y个像素的位置scrollBy(x,y)
: 如果有滚动条,将横向滚动条向左移动x个像素,将纵向滚动条向下移动y个像素window.open()
既可以导航到一个特定的url
,也可以打开一个新的浏览器窗口
如果 window.open()
传递了第二个参数,且该参数是已有窗口或者框架的名称,那么就会在目标窗口加载第一个参数指定的URL
window.open('htttp://www.vue3js.cn','topFrame')
==> < a href=" " target="topFrame"></ a>
window.open()
会返回新窗口的引用,也就是新窗口的 window
对象
const myWin = window.open('http://www.vue3js.cn','myWin')
window.close()
仅用于通过 window.open()
打开的窗口
新创建的 window
对象有一个 opener
属性,该属性指向打开他的原始窗口对象
url
地址如下:
http://foouser:barpassword@www.wrox.com:80/WileyCDA/?q=javascript#contents
location
属性描述如下:
属性名 | 例子 | 说明 |
---|---|---|
hash | “#contents” | utl中#后面的字符,没有则返回空串 |
host | www.wrox.com:80 | 服务器名称和端口号 |
hostname | www.wrox.com | 域名,不带端口号 |
href | http://www.wrox.com:80/WileyCDA/?q=javascript#contents | 完整url |
pathname | “/WileyCDA/” | 服务器下面的文件路径 |
port | 80 | url的端口号,没有则为空 |
protocol | http: | 使用的协议 |
search | ?q=javascript | url的查询字符串,通常为?后面的内容 |
除了 hash
之外,只要修改location
的一个属性,就会导致页面重新加载新 URL
location.reload()
,此方法可以重新刷新当前页面。这个方法会根据最有效的方式刷新页面,如果页面自上一次请求以来没有改变过,页面就会从浏览器缓存中重新加载
如果要强制从服务器中重新加载,传递一个参数true
即可
navigator
对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复杂
下表列出了navigator
对象接口定义的属性和方法:
保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度
history
对象主要用来操作浏览器URL
的历史记录,可以通过参数向前,向后,或者向指定URL
跳转
常用的属性如下:
history.go()
接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转,
history.go('maixaofei.com')
当参数为整数数字的时候,正数表示向前跳转指定的页面,负数为向后跳转指定的页面
history.go(3) //向前跳转三个记录
history.go(-1) //向后跳转一个记录
history.forward()
:向前跳转一个页面history.back()
:向后跳转一个页面history.length
:获取历史记录数javaScript
本地缓存的方法我们主要讲述以下四种:
Cookie
,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP
无状态导致的问题
作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie
有效期、安全性、使用范围的可选属性组成
但是cookie
在每次请求中都会被发送,如果不使用 HTTPS
并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使用 cookie
保持登录态的网站上,如果 cookie
被窃取,他人很容易利用你的 cookie
来假扮成你登录网站
关于cookie
常用的属性如下:
Expires=Wed, 21 Oct 2015 07:28:00 GMT
Expires
高)Max-Age=604800
Domain
指定了 Cookie
可以送达的主机名Path
指定了一个 URL
路径,这个路径必须出现在要请求的资源的路径中才可以发送 Cookie
首部Path=/docs # /docs/Web/ 下的资源会带 Cookie 首部
Secure
的 Cookie
只应通过被HTTPS
协议加密过的请求发送给服务端通过上述,我们可以看到cookie
又开始的作用并不是为了缓存而设计出来,只是借用了cookie
的特性实现缓存
关于cookie
的使用如下:
document.cookie = '名字=值';
关于cookie
的修改,首先要确定domain
和path
属性都是相同的才可以,其中有一个不同得时候都会创建出一个新的cookie
Set-Cookie:name=aa; domain=aa.net; path=/ # 服务端设置
document.cookie =name=bb; domain=aa.net; path=/ # 客户端设置
最后cookie
的删除,最常用的方法就是给cookie
设置一个过期的事件,这样cookie
过期后会被浏览器删除
HTML5
新方法,IE8及以上浏览器都兼容
localStorage
的时候,本页面不会触发storage
事件,但是别的页面会触发storage
事件。localStorage
本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变卡下面再看看关于localStorage
的使用
设置
localStorage.setItem('username','cfangxu');
获取
localStorage.getItem('username')
获取键名
localStorage.key(0) //获取第一个键名
删除
localStorage.removeItem('username')
一次性清除所有存储
localStorage.clear()
localStorage
也不是完美的,它有两个缺点:
Cookie
一样设置过期时间localStorage.setItem('key', {name: 'value'});
console.log(localStorage.getItem('key')); // '[object, Object]'
sessionStorage
和 localStorage
使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage
将会删除数据
indexedDB
是一种低级API,用于客户端存储大量结构化数据(包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索
虽然 Web Storage
对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB
提供了一个解决方案
LocalStorage
同步操作性能更高,尤其是数据量较大时JS
的对象关于indexedDB
的使用基本使用步骤如下:
打开数据库并且开始一个事务
创建一个 object store
构建一个请求来执行一些数据库操作,像增加或提取数据等。
通过监听正确类型的 DOM
事件以等待操作完成。
在操作结果上进行一些操作(可以在 request
对象中找到)
关于使用indexdb
的使用会比较繁琐,大家可以通过使用Godb.js
库进行缓存,最大化的降低操作难度
关于cookie
、sessionStorage
、localStorage
三者的区别主要如下:
存储大小: cookie
数据大小不能超过4k
,sessionStorage
和localStorage
虽然也有存储大小的限制,但比cookie
大得多,可以达到5M或更大
有效时间:localStorage
存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage
数据在当前浏览器窗口关闭后自动删除;cookie
设置的cookie
过期时间之前一直有效,即使窗口或浏览器关闭
数据与服务器之间的交互方式, cookie
的数据会自动的传递到服务器,服务器端也可以写cookie
到客户端; sessionStorage
和localStorage
不会自动把数据发给服务器,仅在本地保存
在了解了上述的前端的缓存方式后,我们可以看看针对不对场景的使用选择:
cookie
localStorage
sessionStorage
indexedDB
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
在 JavaScript
中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁
下面给出一个简单的例子
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();
displayName()
没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量
任何闭包的使用场景都离不开这两点:
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目的
下面举个例子:
在页面上添加一些可以调整字号的按钮
function makeSizer(size) {
return function() {
document.body.style.fontSize = size + 'px';
};
}
var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用
// 假设我们有一个求长方形面积的函数
function getArea(width, height) {
return width * height
}
// 如果我们碰到的长方形的宽老是10
const area1 = getArea(10, 20)
const area2 = getArea(10, 30)
const area3 = getArea(10, 40)
// 我们可以使用闭包柯里化这个计算面积的函数
function getArea(width) {
return height => {
return width * height
}
}
const getTenWidthArea = getArea(10)
// 之后碰到宽度为10的长方形就可以这样计算面积
const area1 = getTenWidthArea(20)
// 而且如果遇到宽度偶尔变化也可以轻松复用
const getTwentyWidthArea = getArea(20)
在JavaScript
中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方法
下面举个例子:
var Counter = (function() {
var privateCounter = 0;
function changeBy(val) {
privateCounter += val;
}
return {
increment: function() {
changeBy(1);
},
decrement: function() {
changeBy(-1);
},
value: function() {
return privateCounter;
}
}
})();
var Counter1 = makeCounter();
var Counter2 = makeCounter();
console.log(Counter1.value()); /* logs 0 */
Counter1.increment();
Counter1.increment();
console.log(Counter1.value()); /* logs 2 */
Counter1.decrement();
console.log(Counter1.value()); /* logs 1 */
console.log(Counter2.value()); /* logs 0 */
上述通过使用闭包来定义公共函数,并令其可以访问私有函数和变量,这种方式也叫模块方式
两个计数器 Counter1
和 Counter2
是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变量
例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期
如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响
例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中。
原因在于每个对象的创建,方法都会被重新赋值
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};
this.getMessage = function() {
return this.message;
};
}
上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下:
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
}
MyObject.prototype.getName = function() {
return this.name;
};
MyObject.prototype.getMessage = function() {
return this.message;
};
简单的来说,执行上下文是一种对Javascript
代码执行环境的抽象概念,也就是说只要有Javascript
代码运行,那么它就一定是运行在执行上下文中
执行上下文的类型分为三种:
window
对象,this
指向这个全局对象eval
函数中的代码,很少用而且不建议使用下面给出全局上下文和函数上下文的例子:
紫色框住的部分为全局上下文,蓝色和橘色框起来的是不同的函数上下文。只有全局上下文(的变量)能被其他任何上下文访问
可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访问
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段即当函数被调用,但未执行任何其内部代码之前
创建阶段做了三件事:
This Binding
伪代码如下:
ExecutionContext = {
ThisBinding = <this value>, // 确定this
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}
确定this
的值我们前面讲到,this
的值是在执行的时候才能确认,定义的时候不能确认
词法环境有两个组成部分:
全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null
,有一个全局对象,this
的值指向这个全局对象
函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments
对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
伪代码如下:
GlobalExectionContext = { // 全局执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <null> // 对外部环境的引用
}
}
FunctionExectionContext = { // 函数执行上下文
LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里 // 对外部环境的引用
outer: <Global or outer function environment reference>
}
}
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性
在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( let
和 const
)绑定,而后者仅用于存储变量( var
)绑定
举个例子
let a = 20;
const b = 30;
var c;
function multiply(e, f) {
var g = 20;
return e * f * g;
}
c = multiply(20, 30);
执行上下文如下:
GlobalExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: { // 词法环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
a: < uninitialized >,
b: < uninitialized >,
multiply: < func >
}
outer: <null>
},
VariableEnvironment: { // 变量环境
EnvironmentRecord: {
Type: "Object",
// 标识符绑定在这里
c: undefined,
}
outer: <null>
}
}
FunctionExectionContext = {
ThisBinding: <Global Object>,
LexicalEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
Arguments: {0: 20, 1: 30, length: 2},
},
outer: <GlobalLexicalEnvironment>
},
VariableEnvironment: {
EnvironmentRecord: {
Type: "Declarative",
// 标识符绑定在这里
g: undefined
},
outer: <GlobalLexicalEnvironment>
}
}
留意上面的代码,let
和const
定义的变量a
和b
在创建阶段没有被赋值,但var
声明的变量从在创建阶段被赋值为undefined
这是因为,创建阶段,会在代码中扫描变量和函数声明,然后将函数声明存储在环境中
但变量会被初始化为undefined
(var
声明的情况下)和保持uninitialized
(未初始化状态)(使用let
和const
声明的情况下)
这就是变量提升的实际原因
在这阶段,执行变量赋值、代码执行
如果 Javascript
引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined
值
执行上下文出栈等待虚拟机回收执行上下文
执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文
当Javascript
引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈中
每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈中
引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下文),当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文
举个例子:
let a = 'Hello World!';
function first() {
console.log('Inside first function');
second();
console.log('Again inside first function');
}
function second() {
console.log('Inside second function');
}
first();
console.log('Inside Global Execution Context');
转化成图的形式
简单分析一下流程:
创建全局上下文请压入执行栈
first
函数被调用,创建函数执行上下文并压入栈
执行first
函数过程遇到second
函数,再创建一个函数执行上下文并压入栈
second
函数执行完毕,对应的函数执行上下文被推出执行栈,执行下一个执行上下文first
函数
first
函数执行完毕,对应的函数执行上下文也被推出栈中,然后执行全局上下文
所有代码执行完毕,全局上下文也会被推出栈中,程序结束
不管怎样简单的需求,在量级达到一定层次时,都会变得异常复杂
文件上传简单,文件变大就复杂
上传大文件时,以下几个变量会影响我们的用户体验
上传时间会变长,高频次文件上传失败,失败后又需要重新上传等等
为了解决上述问题,我们需要对大文件上传单独处理
这里涉及到分片上传及断点续传两个概念
分片上传,就是将所要上传的文件,按照一定的大小,将整个文件分隔成多个数据块(Part)来进行分片上传
如下图
上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文件
大致流程如下:
断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部分
每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度
一般实现方式有两种:
上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即可
如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可
整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完毕
下面的内容都是伪代码
读取文件内容:
const input = document.querySelector('input');
input.addEventListener('change', function() {
var file = this.files[0];
});
可以使用md5
实现文件的唯一性
const md5code = md5(file);
然后开始对文件进行分割
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
//每10M切割一段,这里只做一个切割演示,实际切割需要循环切割,
var slice = e.target.result.slice(0, 10*1024*1024);
});
h5上传一个(一片)
const formdata = new FormData();
formdata.append('0', slice);
//这里是有一个坑的,部分设备无法获取文件名称,和文件类型,这个在最后给出解决方案
formdata.append('filename', file.filename);
var xhr = new XMLHttpRequest();
xhr.addEventListener('load', function() {
//xhr.responseText
});
xhr.open('POST', '');
xhr.send(formdata);
xhr.addEventListener('progress', updateProgress);
xhr.upload.addEventListener('progress', updateProgress);
function updateProgress(event) {
if (event.lengthComputable) {
//进度条
}
}
这里给出常见的图片和视频的文件类型判断
function checkFileType(type, file, back) {
/**
* type png jpg mp4 ...
* file input.change=> this.files[0]
* back callback(boolean)
*/
var args = arguments;
if (args.length != 3) {
back(0);
}
var type = args[0]; // type = '(png|jpg)' , 'png'
var file = args[1];
var back = typeof args[2] == 'function' ? args[2] : function() {};
if (file.type == '') {
// 如果系统无法获取文件类型,则读取二进制流,对二进制进行解析文件类型
var imgType = [
'ff d8 ff', //jpg
'89 50 4e', //png
'0 0 0 14 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 33 67 70 35', //mp4
'0 0 0 0 66 74 79 70 4D 53 4E 56', //mp4
'0 0 0 0 66 74 79 70 69 73 6F 6D', //mp4
'0 0 0 18 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 0 66 74 79 70 6D 70 34 32', //m4v
'0 0 0 14 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 66 74 79 70 71 74 20 20', //mov
'0 0 0 0 6D 6F 6F 76', //mov
'4F 67 67 53 0 02', //ogg
'1A 45 DF A3', //ogg
'52 49 46 46 x x x x 41 56 49 20', //avi (RIFF fileSize fileType LIST)(52 49 46 46,DC 6C 57 09,41 56 49 20,4C 49 53 54)
];
var typeName = [
'jpg',
'png',
'mp4',
'mp4',
'mp4',
'mp4',
'mp4',
'm4v',
'm4v',
'mov',
'mov',
'mov',
'ogg',
'ogg',
'avi',
];
var sliceSize = /png|jpg|jpeg/.test(type) ? 3 : 12;
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
var slice = e.target.result.slice(0, sliceSize);
reader = null;
if (slice && slice.byteLength == sliceSize) {
var view = new Uint8Array(slice);
var arr = [];
view.forEach(function(v) {
arr.push(v.toString(16));
});
view = null;
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
arr = arr.map(function(v) {
if (i > 3 && i < 8) {
return 'x';
}
return v;
});
var idx = arr.join(' ').indexOf(imgType);
if (idx > -1) {
back(typeName[idx]);
} else {
back(false);
}
}
} else {
back(false);
}
});
} else {
var type = file.name.match(/\.(\w+)$/)[1];
back(type);
}
}
调用方法如下
checkFileType('(mov|mp4|avi)',file,function(fileType){
// fileType = mp4,
// 如果file的类型不在枚举之列,则返回false
});
上面上传文件的一步,可以改成:
formdata.append('filename', md5code+'.'+fileType);
有了切割上传后,也就有了文件唯一标识信息,断点续传变成了后台的一个小小的逻辑判断
后端主要做的内容为:根据前端传给后台的md5
值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上传
如果想要暂停切片的上传,可以使用XMLHttpRequest
的 abort
方法
当前的伪代码,只是提供一个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,比如
人生又何尝不是如此,极致的人生体验有无限可能,越是后面才发现越是精彩 _
前面文章我们讲到,JavaScript
中存在两大数据类型:
基本类型数据保存在在栈内存中
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
下面简单实现一个浅拷贝
function shallowClone(obj) {
const newObj = {};
for(let prop in obj) {
if(obj.hasOwnProperty(prop)){
newObj[prop] = obj[prop];
}
}
return newObj;
}
在JavaScript
中,存在浅拷贝的现象有:
Object.assign
Array.prototype.slice()
, Array.prototype.concat()
var obj = {
age: 18,
nature: ['smart', 'good'],
names: {
name1: 'fx',
name2: 'xka'
},
love: function () {
console.log('fx is a great girl')
}
}
var newObj = Object.assign({}, fxObj);
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.slice(0)
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
const fxArr = ["One", "Two", "Three"]
const fxArrs = [...fxArr]
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
_.cloneDeep()
jQuery.extend()
JSON.stringify()
手写循环递归
const _ = require('lodash');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false
const $ = require('jquery');
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
const obj2 = $.extend(true, {}, obj1);
console.log(obj1.b.f === obj2.b.f); // false
const obj2=JSON.parse(JSON.stringify(obj1));
但是这种方式存在弊端,会忽略undefined
、symbol
和函数
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== "object") return obj;
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj);
let cloneObj = new obj.constructor();
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj);
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash);
}
}
return cloneObj;
}
下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别
从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象
// 浅拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj3=shallowClone(obj1) // 一个浅拷贝方法
obj3.name = "update";
obj3.arr[1] = [5,6,7] ; // 新旧对象还是共享同一块内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 5, 6, 7 ], 4 ] }
console.log('obj3',obj3) // obj3 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
// 深拷贝
const obj1 = {
name : 'init',
arr : [1,[2,3],4],
};
const obj4=deepClone(obj1) // 一个深拷贝方法
obj4.name = "update";
obj4.arr[1] = [5,6,7] ; // 新对象跟原对象不共享内存
console.log('obj1',obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4',obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
前提为拷贝类型为引用类型的情况下:
浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
在JavaScript
中,我们可以分成两种类型:
两种类型的区别是:存储位置不同
基本类型主要为以下6种:
数值最常见的整数类型格式则为十进制,还可以设置八进制(零开头)、十六进制(0x开头)
let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum1 = 0xA //16进制的10
浮点类型则在数值汇总必须包含小数点,还可通过科学计数法表示
let floatNum1 = 1.1;
let floatNum2 = 0.1;
let floatNum3 = .1; // 有效,但不推荐
let floatNum = 3.125e7; // 等于 31250000
在数值类型中,存在一个特殊数值NaN
,意为“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误)
console.log(0/0); // NaN
console.log(-0/+0); // NaN
Undefined
类型只有一个值,就是特殊值 undefined
。当使用 var
或 let
声明了变量但没有初始化时,就相当于给变量赋予了 undefined
值
let message;
console.log(message == undefined); // true
包含 undefined
值的变量跟未定义变量是有区别的
let message; // 这个变量被声明了,只是值为 undefined
console.log(message); // "undefined"
console.log(age); // 没有声明过这个变量,报错
字符串可以使用双引号(")、单引号(')或反引号(`)标示
let firstName = "John";
let lastName = 'Jacob';
let lastName = `Jingleheimerschmidt`
字符串是不可变的,意思是一旦创建,它们的值就不能变了
let lang = "Java";
lang = lang + "Script"; // 先销毁再创建
Null
类型同样只有一个值,即特殊值 null
逻辑上讲, null 值表示一个空对象指针,这也是给typeof
传一个 null
会返回 "object"
的原因
let car = null;
console.log(typeof car); // "object"
undefined
值是由 null
值派生而来
console.log(null == undefined); // true
只要变量要保存对象,而当时又没有那个对象可保存,就可用 null
来填充该变量
Boolean
(布尔值)类型有两个字面值: true
和 false
通过Boolean
可以将其他类型的数据转化成布尔值
规则如下:
数据类型 转换为 true 的值 转换为 false 的值
String 非空字符串 ""
Number 非零数值(包括无穷值) 0 、 NaN
Object 任意对象 null
Undefined N/A (不存在) undefined
Symbol (符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false
复杂类型统称为Object
,我们这里主要讲述下面三种:
创建object
常用方式为对象字面量表示法,属性名可以是字符串或数值
let person = {
name: "Nicholas",
"age": 29,
5: true
};
JavaScript
数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是动态大小的,会随着数据添加而自动增长
let colors = ["red", 2, {age: 20 }]
colors.push(2)
函数实际上是对象,每个函数都是 Function
类型的实例,而 Function
也有属性和方法,跟其他引用类型一样
函数存在三种常见的表达方式:
// 函数声明
function sum (num1, num2) {
return num1 + num2;
}
let sum = function(num1, num2) {
return num1 + num2;
};
函数声明和函数表达式两种方式
let sum = (num1, num2) => {
return num1 + num2;
};
除了上述说的三种之外,还包括Date
、RegExp
、Map
、Set
等…
基本数据类型和引用数据类型存储在内存中的位置不同:
基本数据类型存储在栈中
引用类型的对象存储于堆中
当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值
下面来举个例子
let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值
a
的值为一个基本类型,是存储在栈中,将a
的值赋给b
,虽然两个变量的值相等,但是两个变量保存了两个不同的内存地址
下图演示了基本类型赋值的过程:
var obj1 = {}
var obj2 = obj1;
obj2.name = "Xxx";
console.log(obj1.name); // xxx
引用类型数据存放在堆中,每个堆内存对象都有对应的引用地址指向它,引用地址存放在栈中。
obj1
是一个引用类型,在赋值操作过程汇总,实际是将堆内存对象在栈内存的引用地址复制了一份给了obj2
,实际上他们共同指向了同一个堆内存对象,所以更改obj2
会对obj1
产生影响
下图演示这个引用类型赋值过程
本质上是优化高频率执行代码的一种手段
如:浏览器的 resize
、scroll
、keypress
、mousemove
等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能
为了优化体验,需要对这类事件进行调用次数的限制,对此我们就可以采用 防抖(debounce) 和 节流(throttle) 的方式来减少调用频率
一个经典的比喻:
想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应
假设电梯有两种运行策略 debounce
和 throttle
,超时设定为15秒,不考虑容量限制
电梯第一个人进来后,15秒后准时运送一次,这是节流
电梯第一个人进来后,等待15秒。如果过程中又有人进来,15秒等待重新计时,直到15秒后开始运送,这是防抖
完成节流可以使用时间戳与定时器的写法
使用时间戳写法,事件会立即执行,停止触发后没有办法再次执行
function throttled1(fn, delay = 500) {
let oldtime = Date.now()
return function (...args) {
let newtime = Date.now()
if (newtime - oldtime >= delay) {
fn.apply(null, args)
oldtime = Date.now()
}
}
}
使用定时器写法,delay
毫秒后第一次执行,第二次事件停止触发后依然会再一次执行
function throttled2(fn, delay = 500) {
let timer = null
return function (...args) {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, args)
timer = null
}, delay);
}
}
}
可以将时间戳写法的特性与定时器写法的特性相结合,实现一个更加精确的节流。实现如下
function throttled(fn, delay) {
let timer = null
let starttime = Date.now()
return function () {
let curTime = Date.now() // 当前时间
let remaining = delay - (curTime - starttime) // 从上一次到现在,还剩下多少多余时间
let context = this
let args = arguments
clearTimeout(timer)
if (remaining <= 0) {
fn.apply(context, args)
starttime = Date.now()
} else {
timer = setTimeout(fn, remaining);
}
}
}
简单版本的实现
function debounce(func, wait) {
let timeout;
return function () {
let context = this; // 保存this指向
let args = arguments; // 拿到event对象
clearTimeout(timeout)
timeout = setTimeout(function(){
func.apply(context, args)
}, wait);
}
}
防抖如果需要立即执行,可加入第三个参数用于判断,实现如下:
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout); // timeout 不为null
if (immediate) {
let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
timeout = setTimeout(function () {
timeout = null;
}, wait)
if (callNow) {
func.apply(context, args)
}
}
else {
timeout = setTimeout(function () {
func.apply(context, args)
}, wait);
}
}
}
相同点:
setTimeout
实现不同点:
clearTimeout
和 setTimeout
实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能例如,都设置时间频率为500ms,在2秒时间内,频繁触发函数,节流,每隔 500ms 就执行一次。防抖,则不管调动多少次方法,在2s后,只会执行一次
如下图所示:
防抖在连续的事件,只需触发一次回调的场景有:
resize
。只需窗口调整完成后,计算窗口大小。防止重复渲染。节流在间隔一段时间执行一次回调的场景有:
文档对象模型 (DOM) 是 HTML
和 XML
文档的编程接口
它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容
任何 HTML
或XML
文档都可以用 DOM
表示为一个由节点构成的层级结构
节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系,如下所示:
<html>
<head>
<title>Pagetitle>
head>
<body>
<p>Hello World!p >
body>
html>
DOM
像原子包含着亚原子微粒那样,也有很多类型的DOM
节点包含着其他类型的节点。接下来我们先看看其中的三种:
<div>
<p title="title">
content
p >
div>
上述结构中,div
、p
就是元素节点,content
就是文本节点,title
就是属性节点
日常前端开发,我们都离不开DOM
操作
在以前,我们使用Jquery
,zepto
等库来操作DOM
,之后在vue
,Angular
,React
等框架出现后,我们通过操作数据来控制DOM
(绝大多数时候),越来越少的去直接操作DOM
但这并不代表原生操作不重要。相反,DOM
操作才能有助于我们理解框架深层的内容
下面就来分析DOM
常见的操作,主要分为:
创建新元素,接受一个参数,即要创建元素的标签名
const divEl = document.createElement("div");
创建一个文本节点
const textEl = document.createTextNode("content");
用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM
中
const fragment = document.createDocumentFragment();
当请求把一个DocumentFragment
节点插入文档树时,插入的不是 DocumentFragment
自身,而是它的所有子孙节点
创建属性节点,可以是自定义属性
const dataAttribute = document.createAttribute('custom');
consle.log(dataAttribute);
传入任何有效的 css
选择器,即可选中单个 DOM
元素(首个):
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')
如果页面上没有指定的元素时,返回 null
返回一个包含节点子树内所有与之相匹配的Element
节点列表,如果没有相匹配的,则返回一个空节点列表
const notLive = document.querySelectorAll("p");
需要注意的是,该方法返回的是一个 NodeList
的静态实例,它是一个静态的“快照”,而非“实时”的查询
关于获取DOM
元素的方法还有如下,就不一一述说
document.getElementById('id属性值');返回拥有指定id的对象的引用
document.getElementsByClassName('class属性值');返回拥有指定class的对象集合
document.getElementsByTagName('标签名');返回拥有指定标签名的对象集合
document.getElementsByName('name属性值'); 返回拥有指定名称的对象结合
document/element.querySelector('CSS选择器'); 仅返回第一个匹配的元素
document/element.querySelectorAll('CSS选择器'); 返回所有匹配的元素
document.documentElement; 获取页面中的HTML标签
document.body; 获取页面中的BODY标签
document.all['']; 获取页面中的所有元素节点的对象集合型
除此之外,每个DOM
元素还有parentNode
、childNodes
、firstChild
、lastChild
、nextSibling
、previousSibling
属性,关系图如下图所示
不但可以修改一个DOM
节点的文本内容,还可以直接通过HTML
片段修改DOM
节点内部的子树
// 获取...
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // ABC
// 设置HTML:
p.innerHTML = 'ABC RED XYZ';
// ...
的内部结构已修改
自动对字符串进行HTML
编码,保证无法设置任何HTML
标签
// 获取
,拼接到 HTML 中返回给浏览器。形成了如下的 HTML:...
var p = document.getElementById('p-id'); // 设置文本: p.innerText = ''; // HTML被自动编码,无法设置一个<input type="text" value=""><script>alert('XSS');script>"> <button>搜索button> <div> 您搜索的关键词是:"><script>alert('XSS');script> div>
浏览器无法分辨出
是恶意代码,因而将其执行,试想一下,如果是获取
cookie
发送对黑客服务器呢?根据攻击的来源,
XSS
攻击可以分成:
存储型 XSS 的攻击步骤:
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等
反射型 XSS 的攻击步骤:
反射型 XSS 跟存储型 XSS 的区别是:存储型 XSS 的恶意代码存在数据库里,反射型 XSS 的恶意代码存在 URL 里。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。
由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见
DOM 型 XSS 的攻击步骤:
DOM 型 XSS 跟前两种 XSS 的区别:DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞,而其他两种 XSS 都属于服务端的安全漏洞
通过前面介绍,看到XSS
攻击的两大要素:
针对第一个要素,我们在用户输入的过程中,过滤掉用户输入的恶劣代码,然后提交给后端,但是如果攻击者绕开前端请求,直接构造请求就不能预防了
而如果在后端写入数据库前,对输入进行过滤,然后把内容给前端,但是这个内容在不同地方就会有不同显示
例如:
一个正常的用户输入了 5 < 7
这个内容,在写入数据库前,被转义,变成了 5 < 7
在客户端中,一旦经过了 escapeHTML()
,客户端显示的内容就变成了乱码( 5 < 7
)
在前端中,不同的位置所需的编码也不同。
5 < 7
作为 HTML 拼接页面时,可以正常显示:<div title="comment">5 < 7div>
5 < 7
通过 Ajax 返回,然后赋值给 JavaScript 的变量时,前端得到的字符串就是转义后的字符。这个内容不能直接用于 Vue 等模板的展示,也不能直接用于内容长度计算。不能用于标题、alert 等可以看到,过滤并非可靠的,下面就要通过防止浏览器执行恶意代码:
在使用 .innerHTML
、.outerHTML
、document.write()
时要特别小心,不要把不可信的数据作为 HTML 插到页面上,而应尽量使用 .textContent
、.setAttribute()
等
如果用 Vue/React
技术栈,并且不使用 v-html
/dangerouslySetInnerHTML
功能,就在前端 render
阶段避免 innerHTML
、outerHTML
的 XSS 隐患
DOM 中的内联事件监听器,如 location
、onclick
、onerror
、onload
、onmouseover
等, 标签的
href
属性,JavaScript 的 eval()
、setTimeout()
、setInterval()
等,都能把字符串作为代码运行。如果不可信的数据拼接到字符串中传递给这些 API,很容易产生安全隐患,请务必避免
<!-- 链接内包含恶意代码 -->
< a href=" ">1</ a>
<script>
// setTimeout()/setInterval() 中调用恶意代码
setTimeout("UNTRUSTED")
setInterval("UNTRUSTED")
// location 调用恶意代码
location.href = 'UNTRUSTED'
// eval() 中调用恶意代码
eval("UNTRUSTED")
CSRF(Cross-site request forgery)跨站请求伪造:攻击者诱导受害者进入第三方网站,在第三方网站中,向被攻击网站发送跨站请求
利用受害者在被攻击网站已经获取的注册凭证,绕过后台的用户验证,达到冒充用户对被攻击的网站执行某项操作的目
一个典型的CSRF攻击有着如下的流程:
csrf
可以通过get
请求,即通过访问img
的页面后,浏览器自动访问目标地址,发送请求
同样,也可以设置一个自动提交的表单发送post
请求,如下:
<form action="http://bank.example/withdraw" method=POST>
<input type="hidden" name="account" value="xiaoming" />
<input type="hidden" name="amount" value="10000" />
<input type="hidden" name="for" value="hacker" />
</form>
<script> document.forms[0].submit(); </script>
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST
操作
还有一种为使用a
标签的,需要用户点击链接才会触发
访问该页面后,表单会自动提交,相当于模拟用户完成了一次POST操作
< a href="http://test.com/csrf/withdraw.php?amount=1000&for=hacker" taget="_blank">
重磅消息!!
<a/>
CSRF通常从第三方网站发起,被攻击的网站无法防止攻击发生,只能通过增强自己网站针对CSRF的防护能力来提升安全性
防止csrf
常用方案如下:
这里主要讲讲token
这种形式,流程如下:
<input type=”hidden” name=”csrftoken” value=”tokenvalue”/>
Sql 注入攻击,是通过将恶意的 Sql
查询或添加语句插入到应用的输入参数中,再在后台 Sql
服务器上解析执行进行的攻击
流程如下所示:
找出SQL漏洞的注入点
判断数据库的类型以及版本
猜解用户名和密码
利用工具查找Web后台管理入口
入侵和破坏
预防方式如下:
上述只是列举了常见的web
攻击方式,实际开发过程中还会遇到很多安全问题,对于这些问题, 切记不可忽视
单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一
SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统
SSO 一般都需要一个独立的认证中心(passport),子系统的登录均得通过passport
,子系统本身将不参与登录操作
当一个系统成功登录以后,passport
将会颁发一个令牌给各个子系统,子系统可以拿着令牌会获取各自的受保护资源,为了减少频繁认证,各个子系统在被passport
授权以后,会建立一个局部会话,在一定时间内可以无需再次向passport
发起认证
上图有四个系统,分别是Application1
、Application2
、Application3
、和SSO
,当Application1
、Application2
、Application3
需要登录时,将跳到SSO
系统,SSO
系统完成登录,其他的应用系统也就随之登录了
淘宝、天猫都属于阿里旗下,当用户登录淘宝后,再打开天猫,系统便自动帮用户登录了天猫,这种现象就属于单点登录
cookie
的domain
属性设置为当前域的父域,并且父域的cookie
会被子域所共享。path
属性默认为web
应用的上下文路径
利用 Cookie
的这个特点,没错,我们只需要将Cookie
的 domain
属性设置为父域的域名(主域名),同时将 Cookie
的path
属性设置为根路径,将 Session ID
(或 Token
)保存到父域中。这样所有的子域应用就都可以访问到这个Cookie
不过这要求应用系统的域名需建立在一个共同的主域名之下,如 tieba.baidu.com
和 map.baidu.com
,它们都建立在 baidu.com
这个主域名之下,那么它们就可以通过这种方式来实现单点登录
如果是不同域的情况下,Cookie
是不共享的,这里我们可以部署一个认证中心,用于专门处理登录请求的独立的 Web
服务
用户统一在认证中心进行登录,登录成功后,认证中心记录用户的登录状态,并将 token
写入 Cookie
(注意这个 Cookie
是认证中心的,应用系统是访问不到的)
应用系统检查当前请求有没有 Token
,如果没有,说明用户在当前系统中尚未登录,那么就将页面跳转至认证中心
由于这个操作会将认证中心的 Cookie
自动带过去,因此,认证中心能够根据 Cookie
知道用户是否已经登录过了
如果认证中心发现用户尚未登录,则返回登录页面,等待用户登录
如果发现用户已经登录过了,就不会让用户再次登录了,而是会跳转回目标 URL
,并在跳转前生成一个 Token
,拼接在目标 URL
的后面,回传给目标应用系统
应用系统拿到 Token
之后,还需要向认证中心确认下 Token
的合法性,防止用户伪造。确认无误后,应用系统记录用户的登录状态,并将 Token
写入 Cookie
,然后给本次访问放行。(注意这个 Cookie
是当前应用系统的)当用户再次访问当前应用系统时,就会自动带上这个 Token
,应用系统验证 Token 发现用户已登录,于是就不会有认证中心什么事了
此种实现方式相对复杂,支持跨域,扩展性好,是单点登录的标准做法
可以选择将 Session ID
(或 Token
)保存到浏览器的 LocalStorage
中,让前端在每次向后端发送请求时,主动将LocalStorage
的数据传递给服务端
这些都是由前端来控制的,后端需要做的仅仅是在用户登录成功后,将 Session ID
(或 Token
)放在响应体中传递给前端
单点登录完全可以在前端实现。前端拿到 Session ID
(或 Token
)后,除了将它写入自己的 LocalStorage
中之外,还可以通过特殊手段将它写入多个其他域下的 LocalStorage
中
关键代码如下:
// 获取 token
var token = result.data.token;
// 动态创建一个不可见的iframe,在iframe中加载一个跨域HTML
var iframe = document.createElement("iframe");
iframe.src = "http://app1.com/localstorage.html";
document.body.append(iframe);
// 使用postMessage()方法将token传递给iframe
setTimeout(function () {
iframe.contentWindow.postMessage(token, "http://app1.com");
}, 4000);
setTimeout(function () {
iframe.remove();
}, 6000);
// 在这个iframe所加载的HTML中绑定一个事件监听器,当事件被触发时,把接收到的token数据写入localStorage
window.addEventListener('message', function (event) {
localStorage.setItem('token', event.data)
}, false);
前端通过 iframe
+postMessage()
方式,将同一份 Token
写入到了多个域下的 LocalStorage
中,前端每次在向后端发送请求之前,都会主动从 LocalStorage
中读取Token
并在请求中携带,这样就实现了同一份 Token
被多个域所共享
此种实现方式完全由前端控制,几乎不需要后端参与,同样支持跨域
单点登录的流程图如下所示:
用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
sso认证中心发现用户未登录,将用户引导至登录页面
用户输入用户名密码提交登录申请
sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
sso认证中心带着令牌跳转会最初的请求地址(系统1)
系统1拿到令牌,去sso认证中心校验令牌是否有效
sso认证中心校验令牌,返回有效,注册系统1
系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
用户访问系统2的受保护资源
系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
系统2拿到令牌,去sso认证中心校验令牌是否有效
sso认证中心校验令牌,返回有效,注册系统2
系统2使用该令牌创建与用户的局部会话,返回受保护资源
用户登录成功之后,会与sso
认证中心及各个子系统建立会话,用户与sso
认证中心建立的会话称为全局会话
用户与各个子系统建立的会话称为局部会话,局部会话建立之后,用户访问子系统受保护资源将不再通过sso
认证中心
全局会话与局部会话有如下约束关系:
我们也可将字符串常用的操作方法归纳为增、删、改、查,需要知道字符串的特点是一旦创建了,就不可变
这里增的意思并不是说直接增添内容,而是创建字符串的一个副本,再进行操作
除了常用+
以及${}
进行字符串拼接之外,还可通过concat
用于将一个或多个字符串拼接成一个新字符串
let stringValue = "hello ";
let result = stringValue.concat("world");
console.log(result); // "hello world"
console.log(stringValue); // "hello"
这里的删的意思并不是说删除原字符串的内容,而是创建字符串的一个副本,再进行操作
常见的有:
这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数。
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"
这里改的意思也不是改变原字符串,而是创建字符串的一个副本,再进行操作
常见的有:
trim()、trimLeft()、trimRight()
repeat()
padStart()、padEnd()
toLowerCase()、 toUpperCase()
删除前、后或前后所有空格符,再返回新的字符串
let stringValue = " hello world ";
let trimmedStringValue = stringValue.trim();
console.log(stringValue); // " hello world "
console.log(trimmedStringValue); // "hello world"
接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
let stringValue = "na ";
let copyResult = stringValue.repeat(2) // na na
复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件
let stringValue = "foo";
console.log(stringValue.padStart(6)); // " foo"
console.log(stringValue.padStart(9, ".")); // "......foo"
大小写转化
let stringValue = "hello world";
console.log(stringValue.toUpperCase()); // "HELLO WORLD"
console.log(stringValue.toLowerCase()); // "hello world"
除了通过索引的方式获取字符串的值,还可通过:
chatAt()
indexOf()
startWith()
includes()
返回给定索引位置的字符,由传给方法的整数参数指定
let message = "abcde";
console.log(message.charAt(2)); // "c"
从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
let stringValue = "hello world";
console.log(stringValue.indexOf("o")); // 4
从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
let message = "foobarbaz";
console.log(message.startsWith("foo")); // true
console.log(message.startsWith("bar")); // false
console.log(message.includes("bar")); // true
console.log(message.includes("qux")); // false
把字符串按照指定的分割符,拆分成数组中的每一项
let str = "12+23+34"
let arr = str.split("+") // [12,23,34]
针对正则表达式,字符串设计了几个方法:
接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp
对象,返回数组
let text = "cat, bat, sat, fat";
let pattern = /.at/;
let matches = text.match(pattern);
console.log(matches[0]); // "cat"
接收一个参数,可以是一个正则表达式字符串,也可以是一个RegExp
对象,找到则返回匹配索引,否则返回 -1
let text = "cat, bat, sat, fat";
let pos = text.search(/at/);
console.log(pos); // 1
接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"
递归(英语:Recursion)
在数学与计算机科学中,是指在函数的定义中使用函数自身的方法
在函数内部,可以调用其他函数。如果一个函数在内部调用自身本身,这个函数就是递归函数
其核心思想是把一个大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解
一般来说,递归需要有边界条件、递归前进阶段和递归返回阶段。当边界条件不满足时,递归前进;当边界条件满足时,递归返回
下面实现一个函数 pow(x, n)
,它可以计算 x
的 n
次方
使用迭代的方式,如下:
function pow(x, n) {
let result = 1;
// 再循环中,用 x 乘以 result n 次
for (let i = 0; i < n; i++) {
result *= x;
}
return result;
}
使用递归的方式,如下:
function pow(x, n) {
if (n == 1) {
return x;
} else {
return x * pow(x, n - 1);
}
}
pow(x, n)
被调用时,执行分为两个分支:
if n==1 = x
/
pow(x, n) =
\
else = x * pow(x, n - 1)
也就是说pow
递归地调用自身 直到 n == 1
为了计算 pow(2, 4)
,递归变体经过了下面几个步骤:
pow(2, 4) = 2 * pow(2, 3)
pow(2, 3) = 2 * pow(2, 2)
pow(2, 2) = 2 * pow(2, 1)
pow(2, 1) = 2
因此,递归将函数调用简化为一个更简单的函数调用,然后再将其简化为一个更简单的函数,以此类推,直到结果
尾递归,即在函数尾位置调用自身(或是一个尾调用本身的其他函数等等)。尾递归也是递归的一种特殊情形。尾递归是一种特殊的尾调用,即在尾部直接调用自身的递归函数
尾递归在普通尾调用的基础上,多出了2个特征:
在递归调用的过程当中系统为每一层的返回点、局部量等开辟了栈来存储,递归次数过多容易造成栈溢出
这时候,我们就可以使用尾递归,即一个函数中所有递归形式的调用都出现在函数的末尾,对于尾递归来说,由于只存在一个调用记录,所以永远不会发生"栈溢出"错误
实现一下阶乘,如果用普通的递归,如下:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
如果n
等于5,这个方法要执行5次,才返回最终的计算表达式,这样每次都要保存这个方法,就容易造成栈溢出,复杂度为O(n)
如果我们使用尾递归,则如下:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
可以看到,每一次返回的就是一个新的函数,不带上一个函数的参数,也就不需要储存上一个函数了。尾递归只需要保存一个调用栈,复杂度 O(1)
数组求和
function sumArray(arr, total) {
if(arr.length === 1) {
return total
}
return sum(arr, total + arr.pop())
}
使用尾递归优化求斐波那契数列
function factorial2 (n, start = 1, total = 1) {
if(n <= 2){
return total
}
return factorial2 (n -1, total, total + start)
}
数组扁平化
let a = [1,2,3, [1,2,3, [1,2,3]]]
// 变成
let a = [1,2,3,1,2,3,1,2,3]
// 具体实现
function flat(arr = [], result = []) {
arr.forEach(v => {
if(Array.isArray(v)) {
result = result.concat(flat(v, []))
}else {
result.push(v)
}
})
return result
}
数组对象格式化
let obj = {
a: '1',
b: {
c: '2',
D: {
E: '3'
}
}
}
// 转化为如下:
let obj = {
a: '1',
b: {
c: '2',
d: {
e: '3'
}
}
}
// 代码实现
function keysLower(obj) {
let reg = new RegExp("([A-Z]+)", "g");
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
let temp = obj[key];
if (reg.test(key.toString())) {
// 将修改后的属性名重新赋值给temp,并在对象obj内添加一个转换后的属性
temp = obj[key.replace(reg, function (result) {
return result.toLowerCase()
})] = obj[key];
// 将之前大写的键属性删除
delete obj[key];
}
// 如果属性是对象或者数组,重新执行函数
if (typeof temp === 'object' || Object.prototype.toString.call(temp) === '[object Array]') {
keysLower(temp);
}
}
}
return obj;
};
函数的 this
关键字在 JavaScript
中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别
在绝大多数情况下,函数的调用方式决定了 this
的值(运行时绑定)
this
关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象
举个例子:
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是:baz --> bar
// 因此,当前调用位置在baz中
console.log( "bar" );
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是:baz --> bar --> foo
// 因此,当前调用位置在bar中
console.log( "foo" );
}
baz(); // <-- baz的调用位置
同时,this
在函数执行过程中,this
一旦被确定了,就不可以再更改
var a = 10;
var obj = {
a: 20
}
function fn() {
this = obj; // 修改this,运行后会报错
console.log(this.a);
}
fn();
根据不同的使用场合,this
有不同的值,主要分为下面几种情况:
默认绑定
隐式绑定
new绑定
显示绑定
全局环境中定义person
函数,内部使用this
关键字
var name = 'Jenny';
function person() {
return this.name;
}
console.log(person()); //Jenny
上述代码输出Jenny
,原因是调用函数的对象在游览器中位window
,因此this
指向window
,所以输出Jenny
注意:
严格模式下,不能将全局对象用于默认绑定,this会绑定到undefined
,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象
函数还可以作为某个对象的方法调用,这时this
就指这个上级对象
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m(); // 1
这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this
指向的也只是它上一级的对象
var o = {
a:10,
b:{
fn:function(){
console.log(this.a); //undefined
}
}
}
o.b.fn();
上述代码中,this
的上一级对象为b
,b
内部并没有a
变量的定义,所以输出undefined
这里再举一种特殊情况
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //undefined
console.log(this); //window
}
}
}
var j = o.b.fn;
j();
此时this
指向的是window
,这里的大家需要记住,this
永远指向的是最后调用它的对象,虽然fn
是对象b
的方法,但是fn
赋值给j
时候并没有执行,所以最终指向window
通过构建函数new
关键字生成一个实例对象,此时this
指向这个实例对象
function test() {
this.x = 1;
}
var obj = new test();
obj.x // 1
上述代码之所以能过输出1,是因为new
关键字改变了this
的指向
这里再列举一些特殊情况:
new
过程遇到return
一个对象,此时this
指向为返回的对象
function fn()
{
this.user = 'xxx';
return {};
}
var a = new fn();
console.log(a.user); //undefined
如果返回一个简单类型的时候,则this
指向实例对象
function fn()
{
this.user = 'xxx';
return 1;
}
var a = new fn;
console.log(a.user); //xxx
注意的是null
虽然也是对象,但是此时new
仍然指向实例对象
function fn()
{
this.user = 'xxx';
return null;
}
var a = new fn;
console.log(a.user); //xxx
apply()、call()、bind()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this
指的就是这第一个参数
var x = 0;
function test() {
console.log(this.x);
}
var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1
关于apply、call、bind
三者的区别,我们后面再详细说
在 ES6 的语法中还提供了箭头函语法,让我们在代码书写时就能确定 this
的指向(编译时绑定)
举个例子:
const obj = {
sayThis: () => {
console.log(this);
}
};
obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了
const globalSay = obj.sayThis;
globalSay(); // window 浏览器中的 global 对象
虽然箭头函数的this
能够在编译的时候就确定了this
的指向,但也需要注意一些潜在的坑
下面举个例子:
绑定事件监听
const button = document.getElementById('mngb');
button.addEventListener('click', ()=> {
console.log(this === window) // true
this.innerHTML = 'clicked button'
})
上述可以看到,我们其实是想要this
为点击的button
,但此时this
指向了window
包括在原型上添加方法时候,此时this
指向window
Cat.prototype.sayName = () => {
console.log(this === window) //true
return this.name
}
const cat = new Cat('mm');
cat.sayName()
同样的,箭头函数不能作为构建函数
function foo() {
console.log( this.a );
}
var obj1 = {
a: 2,
foo: foo
};
var obj2 = {
a: 3,
foo: foo
};
obj1.foo(); // 2
obj2.foo(); // 3
obj1.foo.call( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
显然,显示绑定的优先级更高
function foo(something) {
this.a = something;
}
var obj1 = {
foo: foo
};
var obj2 = {};
obj1.foo( 2 );
console.log( obj1.a ); // 2
obj1.foo.call( obj2, 3 );
console.log( obj2.a ); // 3
var bar = new obj1.foo( 4 );
console.log( obj1.a ); // 2
console.log( bar.a ); // 4
可以看到,new绑定的优先级>
隐式绑定
new
绑定 VS 显式绑定因为new
和apply、call
无法一起使用,但硬绑定也是显式绑定的一种,可以替换测试
function foo(something) {
this.a = something;
}
var obj1 = {};
var bar = foo.bind( obj1 );
bar( 2 );
console.log( obj1.a ); // 2
var baz = new bar( 3 );
console.log( obj1.a ); // 2
console.log( baz.a ); // 3
bar
被绑定到obj1上,但是new bar(3)
并没有像我们预计的那样把obj1.a
修改为3。但是,new
修改了绑定调用bar()
中的this
我们可认为new
绑定优先级>
显式绑定
综上,new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
前面我们讲到,JS
中有六种简单数据类型:undefined
、null
、boolean
、string
、number
、symbol
,以及引用类型:object
但是我们在声明的时候只有一种数据类型,只有到运行期间才会确定当前类型
let x = y ? 1 : a;
上面代码中,x
的值在编译阶段是无法获取的,只有等到程序运行时才能知道
虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的,如果运算子的类型与预期不符合,就会触发类型转换机制
常见的类型转换有:
显示转换,即我们很清楚可以看到这里发生了类型的转变,常见的方法有:
将任意类型的值转化为数值
先给出类型转换规则:
实践一下:
Number(324) // 324
// 字符串:如果可以被解析为数值,则转换为相应的数值
Number('324') // 324
// 字符串:如果不可以被解析为数值,返回 NaN
Number('324abc') // NaN
// 空字符串转为0
Number('') // 0
// 布尔值:true 转成 1,false 转成 0
Number(true) // 1
Number(false) // 0
// undefined:转成 NaN
Number(undefined) // NaN
// null:转成0
Number(null) // 0
// 对象:通常转换成NaN(除了只包含单个数值的数组)
Number({a: 1}) // NaN
Number([1, 2, 3]) // NaN
Number([5]) // 5
从上面可以看到,Number
转换的时候是很严格的,只要有一个字符无法转成数值,整个字符串就会被转为NaN
parseInt
相比Number
,就没那么严格了,parseInt
函数逐个解析字符,遇到不能转换的字符就停下来
parseInt('32a3') //32
可以将任意类型的值转化成字符串
给出转换规则图:
实践一下:
// 数值:转为相应的字符串
String(1) // "1"
//字符串:转换后还是原来的值
String("a") // "a"
//布尔值:true转为字符串"true",false转为字符串"false"
String(true) // "true"
//undefined:转为字符串"undefined"
String(undefined) // "undefined"
//null:转为字符串"null"
String(null) // "null"
//对象
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"
可以将任意类型的值转为布尔值,转换规则如下:
实践一下:
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false
Boolean({}) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true
在隐式转换中,我们可能最大的疑惑是 :何时发生隐式转换?
我们这里可以归纳为两种情况发生隐式转换的场景:
==
、!=
、>
、<
)、if
、while
需要布尔值地方+
、-
、*
、/
、%
)除了上面的场景,还要求运算符两边的操作数不是同一类型
在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean
函数
可以得出个小结:
除了上面几种会被转化成false
,其他都换被转化成true
遇到预期为字符串的地方,就会将非字符串的值自动转为字符串
具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串
常发生在+
运算中,一旦存在字符串,则会进行字符串拼接操作
'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {} // "5[object Object]"
'5' + [] // "5"
'5' + function (){} // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"
除了+
有可能把运算子转为字符串,其他运算符都会把运算子自动转成数值
'5' - '2' // 3
'5' * '2' // 10
true - 1 // 0
false - 1 // -1
'1' - 1 // 0
'5' * [] // 0
false / '5' // 0
'abc' - 1 // NaN
null + 1 // 1
undefined + 1 // NaN
null
转为数值时,值为0
。undefined
转为数值时,值为NaN
typeof
操作符返回一个字符串,表示未经计算的操作数的类型
使用方法如下:
typeof operand
typeof(operand)
operand
表示对象或原始值的表达式,其类型将被返回
举个例子
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof null // 'object'
typeof [] // 'object'
typeof {} // 'object'
typeof console // 'object'
typeof console.log // 'function'
从上面例子,前6个都是基础数据类型。虽然typeof null
为object
,但这只是 JavaScript
存在的一个悠久 Bug
,不代表null
就是引用数据类型,并且null
本身也不是对象
所以,null
在 typeof
之后返回的是有问题的结果,不能作为判断null
的方法。如果你需要在 if
语句中判断是否为 null
,直接通过===null
来判断就好
同时,可以发现引用类型数据,用typeof
来判断的话,除了function
会被识别出来之外,其余的都输出object
如果我们想要判断一个变量是否存在,可以使用typeof
:(不能使用if(a)
, 若a
未声明,则报错)
if(typeof a != 'undefined'){
//变量存在
}
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上
使用如下:
object instanceof constructor
object
为实例对象,constructor
为构造函数
构造函数通过new
可以实例对象,instanceof
能判断这个对象是否是之前那个构造函数生成的对象
// 定义构建函数
let Car = function() {}
let benz = new Car()
benz instanceof Car // true
let car = new String('xxx')
car instanceof String // true
let str = 'xxx'
str instanceof String // false
关于instanceof
的实现原理,可以参考下面:
function myInstanceof(left, right) {
// 这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false;
// getProtypeOf是Object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left);
while(true) {
if(proto === null) return false;
if(proto === right.prototype) return true;//找到相同原型对象,返回true
proto = Object.getPrototypeof(proto);
}
}
也就是顺着原型链去找,直到找到相同的原型对象,返回true
,否则为false
typeof
与instanceof
都是判断数据类型的方法,区别如下:
typeof
会返回一个变量的基本类型,instanceof
返回的是一个布尔值
instanceof
可以准确地判断复杂引用数据类型,但是不能正确判断基础数据类型
而 typeof
也存在弊端,它虽然可以判断基础数据类型(null
除外),但是引用数据类型中,除了 function
类型以外,其他的也无法判断
可以看到,上述两种方法都有弊端,并不能满足所有场景的需求
如果需要通用检测数据类型,可以采用Object.prototype.toString
,调用该方法,统一返回格式“[object Xxx]”
的字符串
如下
Object.prototype.toString({}) // "[object Object]"
Object.prototype.toString.call({}) // 同上结果,加上call也ok
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call('1') // "[object String]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(function(){}) // "[object Function]"
Object.prototype.toString.call(null) //"[object Null]"
Object.prototype.toString.call(undefined) //"[object Undefined]"
Object.prototype.toString.call(/123/g) //"[object RegExp]"
Object.prototype.toString.call(new Date()) //"[object Date]"
Object.prototype.toString.call([]) //"[object Array]"
Object.prototype.toString.call(document) //"[object HTMLDocument]"
Object.prototype.toString.call(window) //"[object Window]"
了解了toString
的基本用法,下面就实现一个全局通用的数据类型判断方法
function getType(obj){
let type = typeof obj;
if (type !== "object") { // 先进行typeof判断,如果是基础数据类型,直接返回
return type;
}
// 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1');
}
使用如下
getType([]) // "Array" typeof []是object,因此toString返回
getType('123') // "string" typeof 直接返回
getType(window) // "Window" toString返回
getType(null) // "Null"首字母大写,typeof null是object,需toString来判断
getType(undefined) // "undefined" typeof 直接返回
getType() // "undefined" typeof 直接返回
getType(function(){}) // "function" typeof能判断,因此首字母小写
getType(/123/g) //"RegExp" toString返回
可视区域即我们浏览网页的设备肉眼可见的区域,如下图
在日常开发中,我们经常需要判断目标元素是否在视窗之内或者和视窗的距离小于一个值(例如 100 px),从而实现一些常用的功能,例如:
判断一个元素是否在可视区域,我们常用的有三种办法:
offsetTop、scrollTop
getBoundingClientRect
Intersection Observer
offsetTop
,元素的上外边框至包含元素的上内边框之间的像素距离,其他offset
属性如下图所示:
下面再来了解下clientWidth
、clientHeight
:
clientWidth
:元素内容区宽度加上左右内边距宽度,即clientWidth = content + padding
clientHeight
:元素内容区高度加上上下内边距高度,即clientHeight = content + padding
这里可以看到client
元素都不包括外边距
最后,关于scroll
系列的属性如下:
scrollWidth
和 scrollHeight
主要用于确定元素内容的实际大小
scrollLeft
和 scrollTop
属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位置
scrollTop > 0
scrollLeft > 0
将元素的 scrollLeft
和 scrollTop
设置为 0,可以重置元素的滚动位置
下面再看看如何实现判断:
公式如下:
el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
代码实现:
function isInViewPortOfOne (el) {
// viewPortHeight 兼容所有浏览器写法
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
return top <= viewPortHeight
}
返回值是一个 DOMRect
对象,拥有left
, top
, right
, bottom
, x
, y
, width
, 和 height
属性
const target = document.querySelector('.target');
const clientRect = target.getBoundingClientRect();
console.log(clientRect);
// {
// bottom: 556.21875,
// height: 393.59375,
// left: 333,
// right: 1017,
// top: 162.625,
// width: 684
// }
属性对应的关系图如下所示:
当页面发生滚动的时候,top
与left
属性值都会随之改变
如果一个元素在视窗之内的话,那么它一定满足下面四个条件:
实现代码如下:
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left,
} = element.getBoundingClientRect();
return (
top >= 0 &&
left >= 0 &&
right <= viewWidth &&
bottom <= viewHeight
);
}
Intersection Observer
即重叠观察者,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect
会好很多
使用步骤主要分为两步:创建观察者和传入被观察者
const options = {
// 表示重叠面积占被观察者的比例,从 0 - 1 取值,
// 1 表示完全被包含
threshold: 1.0,
root:document.querySelector('#scrollArea') // 必须是目标元素的父级元素
};
const callback = (entries, observer) => { ....}
const observer = new IntersectionObserver(callback, options);
通过new IntersectionObserver
创建了观察者 observer
,传入的参数 callback
在重叠比例超过 threshold
时会被执行`
关于callback
回调函数常用属性如下:
// 上段代码中被省略的 callback
const callback = function(entries, observer) {
entries.forEach(entry => {
entry.time; // 触发的时间
entry.rootBounds; // 根元素的位置矩形,这种情况下为视窗位置
entry.boundingClientRect; // 被观察者的位置举行
entry.intersectionRect; // 重叠区域的位置矩形
entry.intersectionRatio; // 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
entry.target; // 被观察者
});
};
通过 observer.observe(target)
这一行代码即可简单的注册被观察者
const target = document.querySelector('.target');
observer.observe(target);
实现:创建了一个十万个节点的长列表,当节点滚入到视窗中时,背景就会从红色变为黄色
Html
结构如下:
<div class="container"></div>
css
样式如下:
.container {
display: flex;
flex-wrap: wrap;
}
.target {
margin: 5px;
width: 20px;
height: 20px;
background: red;
}
往container
插入1000个元素
const $container = $(".container");
// 插入 100000 个
function createTargets() {
const htmlString = new Array(100000)
.fill('')
.join("");
$container.html(htmlString);
}
这里,首先使用getBoundingClientRect
方法进行判断元素是否在可视区域
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight =
window.innerHeight || document.documentElement.clientHeight;
const { top, right, bottom, left } = element.getBoundingClientRect();
return top >= 0 && left >= 0 && right <= viewWidth && bottom <= viewHeight;
}
然后开始监听scroll
事件,判断页面上哪些元素在可视区域中,如果在可视区域中则将背景颜色设置为yellow
$(window).on("scroll", () => {
console.log("scroll !");
$targets.each((index, element) => {
if (isInViewPort(element)) {
$(element).css("background-color", "yellow");
}
});
});
通过上述方式,可以看到可视区域颜色会变成黄色了,但是可以明显看到有卡顿的现象,原因在于我们绑定了scroll
事件,scroll
事件伴随了大量的计算,会造成资源方面的浪费
下面通过Intersection Observer
的形式同样实现相同的功能
首先创建一个观察者
const observer = new IntersectionObserver(getYellow, { threshold: 1.0 });
getYellow
回调函数实现对背景颜色改变,如下:
function getYellow(entries, observer) {
entries.forEach(entry => {
$(entry.target).css("background-color", "yellow");
});
}
最后传入观察者,即.target
元素
$targets.each((index, element) => {
observer.observe(element);
});
可以看到功能同样完成,并且页面不会出现卡顿的情况
https://download.csdn.net/download/suli77/87411227