Tiny'Wo | 小窝

网络中的一小块自留地

面试官:说说你对BOM的理解,常见的BOM对象你了解哪些?

一、是什�?

BOM (Browser Object Model),浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象

其作用就是跟浏览器做一些交互效�?比如如何进行页面的后退,前进,刷新,浏览器的窗口发生变化,滚动条的滚动,以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率

浏览器的全部内容可以看成DOM,整个浏览器可以看成BOM。区别如下:

二、window

Bom的核心对象是window,它表示浏览器的一个实�?
在浏览器中,window对象有双重角色,即是浏览器窗口的一个接口,又是全局对象

因此所有在全局作用域中声明的变量、函数都会变成window对象的属性和方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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

```js
window.open('htttp://www.vue3js.cn','topFrame')
==> < a href=" " target="topFrame"></ a>

window.open() 会返回新窗口的引用,也就是新窗口�?window 对象

1
const myWin = window.open('http://www.vue3js.cn','myWin')

window.close() 仅用于通过 window.open() 打开的窗�?
新创建的 window 对象有一�?opener 属性,该属性指向打开他的原始窗口对象

三、location

url地址如下�?

1
http://foouser:[email protected]: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 对象主要用来获取浏览器的属性,区分浏览器类型。属性较多,且兼容性比较复�?
下表列出了navigator对象接口定义的属性和方法�?

五、screen

保存的纯粹是客户端能力信息,也就是浏览器窗口外面的客户端显示器的信息,比如像素宽度和像素高度

六、history

history对象主要用来操作浏览器URL的历史记录,可以通过参数向前,向后,或者向指定URL跳转

常用的属性如下:

  • history.go()

接收一个整数数字或者字符串参数:向最近的一个记录中包含指定字符串的页面跳转�?

1
history.go('maixaofei.com')

当参数为整数数字的时候,正数表示向前跳转指定的页面,负数为向后跳转指定的页面

history.go(3) //向前跳转三个记录
history.go(-1) //向后跳转一个记�?```

- `history.forward()`:向前跳转一个页�?- `history.back()`:向后跳转一个页�?- `history.length`:获取历史记录数

面试官:DOM常见的操作有哪些�?

一、DOM

文档对象模型 (DOM) �?HTML �?XML 文档的编程接�?
它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容

任何 HTML XML文档都可以用 DOM 表示为一个由节点构成的层级结�?
节点分很多类型,每种类型对应着文档中不同的信息和(或)标记,也都有自己不同的特性、数据和方法,而且与其他类型有某种关系,如下所示:

1
2
3
4
5
6
7
8
<html>
<head>
<title>Page</title>
</head>
<body>
<p>Hello World!</p >
</body>
</html>

DOM像原子包含着亚原子微粒那样,也有很多类型的DOM节点包含着其他类型的节点。接下来我们先看看其中的三种�?

1
2
3
4
5
<div>
<p title="title">
content
</p >
</div>

上述结构中,divp就是元素节点,content就是文本节点,title就是属性节�?

二、操�?

日常前端开发,我们都离不开DOM操作

在以前,我们使用Jqueryzepto等库来操作DOM,之后在vueAngularReact等框架出现后,我们通过操作数据来控制DOM(绝大多数时候),越来越少的去直接操作DOM

但这并不代表原生操作不重要。相反,DOM操作才能有助于我们理解框架深层的内容

下面就来分析DOM常见的操作,主要分为�?

  • 创建节点
  • 查询节点
  • 更新节点
  • 添加节点
  • 删除节点

创建节点

createElement

创建新元素,接受一个参数,即要创建元素的标签名

1
const divEl = document.createElement("div");

createTextNode

创建一个文本节�?

1
const textEl = document.createTextNode("content");

createDocumentFragment

用来创建一个文档碎片,它表示一种轻量级的文档,主要是用来存储临时节点,然后把文档碎片的内容一次性添加到DOM�?

1
const fragment = document.createDocumentFragment();

当请求把一个DocumentFragment 节点插入文档树时,插入的不是 DocumentFragment 自身,而是它的所有子孙节�?

createAttribute

创建属性节点,可以是自定义属�?

1
2
const dataAttribute = document.createAttribute('custom');
consle.log(dataAttribute);

获取节点

querySelector

传入任何有效的 css 选择器,即可选中单个 DOM 元素(首个)�?

1
2
3
4
5
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')
document.querySelector('[name="username"]')
document.querySelector('div + p > span')

如果页面上没有指定的元素时,返回 null

querySelectorAll

返回一个包含节点子树内所有与之相匹配的Element节点列表,如果没有相匹配的,则返回一个空节点列表

1
const notLive = document.querySelectorAll("p");

需要注意的是,该方法返回的是一�?NodeList 的静态实例,它是一个静态的“快照”,而非“实时”的查询

关于获取DOM元素的方法还有如下,就不一一述说

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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`属性,关系图如下图所�?
![](https://static.vue-js.com/c100f450-7fdc-11eb-ab90-d9ae814b240d.png)



### 更新节点

#### innerHTML

不但可以修改一个`DOM`节点的文本内容,还可以直接通过`HTML`片段修改`DOM`节点内部的子�?
```js
// 获取<p id="p">...</p >
var p = document.getElementById('p');
// 设置文本为abc:
p.innerHTML = 'ABC'; // <p id="p">ABC</p >
// 设置HTML:
p.innerHTML = 'ABC <span style="color:red">RED</span> XYZ';
// <p>...</p >的内部结构已修改

innerText、textContent

自动对字符串进行HTML编码,保证无法设置任何HTML标签

1
2
3
4
5
6
// 获取<p id="p-id">...</p >
var p = document.getElementById('p-id');
// 设置文本:
p.innerText = '<script>alert("Hi")</script>';
// HTML被自动编码,无法设置一�?script>节点:
// <p id="p-id">&lt;script&gt;alert("Hi")&lt;/script&gt;</p >

两者的区别在于读取属性时,innerText不返回隐藏元素的文本,而textContent返回所有文�?

style

DOM节点的style属性对应所有的CSS,可以直接获取或设置。遇到-需要转化为驼峰命名

1
2
3
4
5
6
// 获取<p id="p-id">...</p >
const p = document.getElementById('p-id');
// 设置CSS:
p.style.color = '#ff0000';
p.style.fontSize = '20px'; // 驼峰命名
p.style.paddingTop = '2em';

添加节点

innerHTML

如果这个DOM节点是空的,例如,<div></div>,那么,直接使用innerHTML = '<span>child</span>'就可以修改DOM节点的内容,相当于添加了新的DOM节点

如果这个DOM节点不是空的,那就不能这么做,因为innerHTML会直接替换掉原来的所有子节点

appendChild

把一个子节点添加到父节点的最后一个子节点

举个例子

1
2
3
4
5
6
7
<!-- HTML结构 -->
<p id="js">JavaScript</p >
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
</div>

添加一个p元素

1
2
3
4
const js = document.getElementById('js')
js.innerHTML = "JavaScript"
const list = document.getElementById('list');
list.appendChild(js);

现在HTML结构变成了下�?

1
2
3
4
5
6
7
<!-- HTML结构 -->
<div id="list">
<p id="java">Java</p >
<p id="python">Python</p >
<p id="scheme">Scheme</p >
<p id="js">JavaScript</p > <!-- 添加元素 -->
</div>

上述代码中,我们是获取DOM元素后再进行添加操作,这个js节点是已经存在当前文档树中,因此这个节点首先会从原先的位置删除,再插入到新的位置

如果动态添加新的节点,则先创建一个新的节点,然后插入到指定的位置

1
2
3
4
5
const list = document.getElementById('list'),
const haskell = document.createElement('p');
haskell.id = 'haskell';
haskell.innerText = 'Haskell';
list.appendChild(haskell);

insertBefore

把子节点插入到指定的位置,使用方法如下:

1
parentElement.insertBefore(newElement, referenceElement)

子节点会插入到referenceElement之前

setAttribute

在指定元素中添加一个属性节点,如果元素中已有该属性改变属性�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const div = document.getElementById('id')
div.setAttribute('class', 'white');//第一个参数属性名,第二个参数属性值�?```



### 删除节点

删除一个节点,首先要获得该节点本身以及它的父节点,然后,调用父节点的`removeChild`把自己删�?
```js
// 拿到待删除节�?
const self = document.getElementById('to-be-removed');
// 拿到父节�?
const parent = self.parentElement;
// 删除:
const removed = parent.removeChild(self);
removed === self; // true

删除后的节点虽然不在文档树中了,但其实它还在内存中,可以随时再次被添加到别的位置

相关链接

https://developer.mozilla.org/zh-CN/docs/Web/API/Document_Object_Model

面试官:ajax原理是什么?如何实现�?

一、是什�?

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() 构造函数用于初始化一�?XMLHttpRequest 实例对象

1
const xhr = new XMLHttpRequest();

与服务器建立连接

通过 XMLHttpRequest 对象�?open() 方法与服务器建立连接

1
xhr.open(method, url, [async][, user][, password])

参数说明�?

  • method:表示当前的请求方式,常见的有GETPOST

  • url:服务端地址

  • async:布尔值,表示是否异步执行操作,默认为true

  • user: 可选的用户名用于认证用途;默认为`null

  • password: 可选的密码用于认证用途,默认为`null

给服务端发送数�?

通过 XMLHttpRequest 对象�?send() 方法,将客户端页面的数据发送给服务�?

1
xhr.send([body])

body: �?XHR 请求中要发送的数据体,如果不传递数据则�?null

如果使用GET请求发送数据的时候,需要注意如下:

  • 将请求数据添加到open()方法中的url地址�?- 发送请求数据中的send()方法中参数设置为null

绑定onreadystatechange事件

onreadystatechange 事件用于监听服务器端的通信状态,主要监听的属性为XMLHttpRequest.readyState ,

关于XMLHttpRequest.readyState属性有五个状态,如下图显�?

只要 readyState 属性值一变化,就会触发一�?readystatechange 事件

XMLHttpRequest.responseText属性用于接收服务器端的响应结果

举个例子�?

1
2
3
4
5
6
7
8
9
10
11
12
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请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
//封装一个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)
}
}
}
}

使用方式如下

1
2
3
4
5
6
7
8
9
10
11
12
ajax({
type: 'post',
dataType: 'json',
data: {},
url: 'https://xxxx',
success: function(text,xml){//请求成功后的回调函数
console.log(text)
},
fail: function(status){////请求失败后的回调函数
console.log(status)
}
})

面试官:数组的常用方法有哪些�?

一、操作方�?

数组基本操作可以归纳�?增、删、改、查,需要留意的是哪些方法会对原数组产生影响,哪些方法不�?
下面对数组常用的操作方法做一个归�?

�?

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

  • push()
  • unshift()
  • splice()
  • concat()

push()

push()方法接收任意数量的参数,并将它们添加到数组末尾,返回数组的最新长�?

1
2
let colors = []; // 创建一个数�?let count = colors.push("red", "green"); // 推入两项
console.log(count) // 2

unshift()

unshift()在数组开头添加任意多个值,然后返回新的数组长度

1
let colors = new Array(); // 创建一个数�?let count = colors.unshift("red", "green"); // 从数组开头推入两�?alert(count); // 2

splice

传入三个参数,分别是开始位置�?(要删除的元素数量)、插入的元素,返回空数组

1
2
3
4
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 0, "yellow", "orange")
console.log(colors) // red,yellow,orange,green,blue
console.log(removed) // []

concat()

首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组,不会影响原始数组

1
2
3
4
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()
  • shift()
  • splice()
  • slice()

pop()

pop() 方法用于删除数组的最后一项,同时减少数组的 length 值,返回被删除的�?

1
2
3
let colors = ["red", "green"]
let item = colors.pop(); // 取得最后一�?console.log(item) // green
console.log(colors.length) // 1

shift()

shift()方法用于删除数组的第一项,同时减少数组的 length 值,返回被删除的�?

1
2
3
let colors = ["red", "green"]
let item = colors.shift(); // 取得第一�?console.log(item) // red
console.log(colors.length) // 1

splice()

传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数�?

1
2
3
let colors = ["red", "green", "blue"];
let removed = colors.splice(0,1); // 删除第一�?console.log(colors); // green,blue
console.log(removed); // red,只有一个元素的数组

slice()

slice() 用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数�?

1
2
3
4
5
6
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

splice()

传入三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响

1
2
3
let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元�?console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组

�?

即查找元素,返回元素坐标或者元素�?

  • indexOf()
  • includes()
  • find()

indexOf()

返回要查找的元素在数组中的位置,如果没找到则返回 -1

1
2
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.indexOf(4) // 3

includes()

返回要查找的元素在数组中的位置,找到返回true,否则false

1
2
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.includes(4) // true

find()

返回第一个匹配的元素

1
2
3
4
5
6
7
8
9
10
11
const people = [
{
name: "Matt",
age: 27
},
{
name: "Nicholas",
age: 29
}
];
people.find((element, index, array) => element.age < 28) // // {name: "Matt", age: 27}

二、排序方�?

数组有两个方法可以用来对元素重新排序�?

  • reverse()
  • sort()

reverse()

顾名思义,将数组元素方向反转

1
2
3
let values = [1, 2, 3, 4, 5];
values.reverse();
alert(values); // 5,4,3,2,1

sort()

sort()方法接受一个比较函数,用于判断哪个值应该排在前�?

1
2
3
4
5
6
7
8
9
10
11
12
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()

join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串

1
2
3
let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

四、迭代方�?

常用来迭代数组的方法(都不改变原数组)有如下�?

  • some()
  • every()
  • forEach()
  • filter()
  • map()

some()

对数组每一项都运行传入的测试函数,如果至少�?个元素返�?true ,则这个方法返回 true

1
2
3
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let someResult = numbers.some((item, index, array) => item > 2);
console.log(someResult) // true

every()

对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true

1
2
3
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
let everyResult = numbers.every((item, index, array) => item > 2);
console.log(everyResult) // false

forEach()

对数组每一项都运行传入的函数,没有返回�?

1
2
3
4
let numbers = [1, 2, 3, 4, 5, 4, 3, 2, 1];
numbers.forEach((item, index, array) => {
// 执行某些操作
});

filter()

对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返�?

1
2
3
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

map()

对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数�?

1
2
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

面试官:bind、call、apply 区别?如何实现一个bind?

一、作�?

call apply bind 作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向

那么什么情况下需要改变this的指向呢?下面举个例�?

1
2
3
4
5
6
7
8
9
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指向�?

1
setTimeout(obj.say.bind(obj),0); //martin,this指向obj对象

二、区�?

下面再来看看applycallbind的使�?

apply

apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入

改变this指向后原函数会立即执行,且此方法只是临时改变this指向一�?

1
2
3
4
5
6
7
8
9
function fn(...args){
console.log(this,args);
}
let obj = {
myname:"张三"
}

fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

当第一个参数为nullundefined的时候,默认指向window(在浏览器�?

1
2
fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window

call

call方法的第一个参数也是this的指向,后面传入的是一个参数列�?
apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一�?

1
2
3
4
5
6
7
8
9
function fn(...args){
console.log(this,args);
}
let obj = {
myname:"张三"
}

fn.call(obj,1,2); // this会变成传入的obj,传入的参数必须是一个数组;
fn(1,2) // this指向window

同样的,当第一个参数为nullundefined的时候,默认指向window(在浏览器�?

1
2
fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

bind

bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列�?但是这个参数列表可以分多次传�?

改变this指向后不会立即执行,而是返回一个永久改变this指向的函�?

1
2
3
4
5
6
7
8
9
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

小结

从上面可以看到,applycallbind三者的区别在于�?

  • 三者都可以改变函数的this对象指向
  • 三者第一个参数都是this要指向的对象,如果如果没有这个参数或参数为undefinednull,则默认指向全局window
  • 三者都可以传参,但是apply是数组,而call是参数列表,且applycall是一次性传入参数,而bind可以分为多次传入
  • bind 是返回绑定this之后的函数,apply call 则是立即执行

三、实�?

实现bind的步骤,我们可以分解成为三部分:

  • 修改this指向

  • 动态传递参�?

    1
    2
    3
    // 方式一:只在bind中传递函数参�?fn.bind(obj,1,2)()

    // 方式二:在bind中传递函数参数,也在返回函数中传递参�?fn.bind(obj,1)(2)
  • 兼容new关键�?
    整体实现代码如下�?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    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));
    }
    }

面试官:Javascript本地存储的方式有哪些?区别及应用场景�?

一、方�?

javaScript本地缓存的方法我们主要讲述以下四种:

  • cookie
  • sessionStorage
  • localStorage
  • indexedDB

Cookie,类型为「小型文本文件」,指某些网站为了辨别用户身份而储存在用户本地终端上的数据。是为了解决 HTTP 无状态导致的问题

作为一段一般不超过 4KB 的小型文本数据,它由一个名称(Name)、一个值(Value)和其它几个用于控制 cookie 有效期、安全性、使用范围的可选属性组�?
但是cookie在每次请求中都会被发送,如果不使�?HTTPS 并对其加密,其保存的信息很容易被窃取,导致安全风险。举个例子,在一些使�?cookie 保持登录态的网站上,如果 cookie 被窃取,他人很容易利用你�?cookie 来假扮成你登录网�?
关于cookie常用的属性如下:

  • Expires 用于设置 Cookie 的过期时�?

    1
    Expires=Wed, 21 Oct 2015 07:28:00 GMT
  • Max-Age 用于设置�?Cookie 失效之前需要经过的秒数(优先级比Expires高)

1
Max-Age=604800
  • Domain 指定�?Cookie 可以送达的主机名
  • Path 指定了一�?URL 路径,这个路径必须出现在要请求的资源的路径中才可以发�?Cookie 首部
1
Path=/docs   # /docs/Web/ 下的资源会带 Cookie 首部
  • 标记�?Secure �?Cookie 只应通过被HTTPS协议加密过的请求发送给服务�?
    通过上述,我们可以看到cookie又开始的作用并不是为了缓存而设计出来,只是借用了cookie的特性实现缓�?
    关于cookie的使用如下:
1
document.cookie = '名字=�?;

关于cookie的修改,首先要确定domainpath属性都是相同的才可以,其中有一个不同得时候都会创建出一个新的cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Set-Cookie:name=aa; domain=aa.net; path=/  # 服务端设�?document.cookie =name=bb; domain=aa.net; path=/  # 客户端设�?```

最后`cookie`的删除,最常用的方法就是给`cookie`设置一个过期的事件,这样`cookie`过期后会被浏览器删除



### localStorage

`HTML5`新方法,IE8及以上浏览器都兼�?
### 特点

- 生命周期:持久化的本地存储,除非主动删除数据,否则数据是永远不会过期�?- 存储的信息在同一域中是共享的
- 当本页操作(新增、修改、删除)了`localStorage`的时候,本页面不会触发`storage`事件,但是别的页面会触发`storage`事件�?- 大小�?M(跟浏览器厂商有关系�?- `localStorage`本质上是对字符串的读取,如果存储内容多的话会消耗内存空间,会导致页面变�?- 受同源策略的限制

下面再看看关于`localStorage`的使�?
设置

```js
localStorage.setItem('username','cfangxu');

获取

1
localStorage.getItem('username')

获取键名

1
2
3
4
5
6
localStorage.key(0) //获取第一个键�?```

删除

```js
localStorage.removeItem('username')

一次性清除所有存�?

1
localStorage.clear()

localStorage 也不是完美的,它有两个缺点:

  • 无法像Cookie一样设置过期时�?- 只能存入字符串,无法直接存对�?
    1
    2
    localStorage.setItem('key', {name: 'value'});
    console.log(localStorage.getItem('key')); // '[object, Object]'

sessionStorage

sessionStorage �?localStorage 使用方法基本一致,唯一不同的是生命周期,一旦页面(会话)关闭,sessionStorage 将会删除数据

扩展的前端存储方�?

indexedDB 是一种低级API,用于客户端存储大量结构化数�?包括, 文件/ blobs)。该API使用索引来实现对该数据的高性能搜索

虽然 Web Storage 对于存储较少量的数据很有用,但对于存储更大量的结构化数据来说,这种方法不太有用。IndexedDB提供了一个解决方�?

优点�?

  • 储存量理论上没有上限
  • 所有操作都是异步的,相�?LocalStorage 同步操作性能更高,尤其是数据量较大时
  • 原生支持储存JS的对�?- 是个正经的数据库,意味着数据库能干的事它都能�?

缺点�?

  • 操作非常繁琐

  • 本身有一定门�?
    关于indexedDB的使用基本使用步骤如下:

  • 打开数据库并且开始一个事�?

  • 创建一�?object store

  • 构建一个请求来执行一些数据库操作,像增加或提取数据等�?- 通过监听正确类型�?DOM 事件以等待操作完成�?- 在操作结果上进行一些操作(可以�?request 对象中找到)

关于使用indexdb的使用会比较繁琐,大家可以通过使用Godb.js库进行缓存,最大化的降低操作难�?

二、区�?

关于cookiesessionStoragelocalStorage三者的区别主要如下�?

  • 存储大小: cookie数据大小不能超过4ksessionStoragelocalStorage 虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更�?

  • 有效时间:localStorage 存储持久数据,浏览器关闭后数据不丢失除非主动删除数据�?sessionStorage 数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭

  • 数据与服务器之间的交互方式, cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端�?sessionStoragelocalStorage不会自动把数据发给服务器,仅在本地保�?

三、应用场�?

在了解了上述的前端的缓存方式后,我们可以看看针对不对场景的使用选择�?

  • 标记用户与跟踪用户行为的情况,推荐使用cookie
  • 适合长期保存在本地的数据(令牌),推荐使用localStorage
  • 敏感账号一次性登录,推荐使用sessionStorage
  • 存储大量数据的情况、在线文档(富文本编辑器)保存编辑历史的情况,推荐使用indexedDB

相关连接

面试官:说说你对闭包的理解?闭包使用场景

一、是什�?

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure�?
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域

�?JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥�?
下面给出一个简单的例子

1
2
3
4
5
6
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变�? function displayName() { // displayName() 是内部函数,一个闭�? alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();

displayName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变�?

二、使用场�?

任何闭包的使用场景都离不开这两点:

  • 创建私有变量
  • 延长变量的生命周�?

    一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但创建时所在词法环境依然存在,以达到延长变量的生命周期的目�?

下面举个例子�?
在页面上添加一些可以调整字号的按钮

1
2
3
4
5
6
7
8
9
10
11
12
13
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;

柯里化函�?

柯里化的目的在于避免频繁调用具有相同参数函数的同时,又能够轻松的重用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 假设我们有一个求长方形面积的函数
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)
// 之后碰到宽度�?0的长方形就可以这样计算面�?const area1 = getTenWidthArea(20)

// 而且如果遇到宽度偶尔变化也可以轻松复�?const getTwentyWidthArea = getArea(20)

使用闭包模拟私有方法

JavaScript中,没有支持声明私有变量,但我们可以使用闭包来模拟私有方�?

下面举个例子�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var 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 是维护它们各自的独立性的,每次调用其中一个计数器时,通过改变这个变量的值,会改变这个闭包的词法环境,不会影响另一个闭包中的变�?

其他

例如计数器、延迟调用、回调等闭包的应用,其核心思想还是创建私有变量和延长变量的生命周期

三、注意事�?

如果不是某些特定任务需要使用闭包,在其它函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响

例如,在创建新的对象或者类时,方法通常应该关联于对象的原型,而不是定义到对象的构造器中�?
原因在于每个对象的创建,方法都会被重新赋�?

1
2
3
4
5
6
7
8
9
10
11
function MyObject(name, message) {
this.name = name.toString();
this.message = message.toString();
this.getName = function() {
return this.name;
};

this.getMessage = function() {
return this.message;
};
}

上面的代码中,我们并没有利用到闭包的好处,因此可以避免使用闭包。修改成如下�?

1
2
3
4
5
6
7
8
9
10
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代码执行环境的抽象概念,也就是说只要有Javascript代码运行,那么它就一定是运行在执行上下文�?
执行上下文的类型分为三种�?

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window 对象,this 指向这个全局对象
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使�?
    下面给出全局上下文和函数上下文的例子�?

紫色框住的部分为全局上下文,蓝色和橘色框起来的是不同的函数上下文。只有全局上下文(的变量)能被其他任何上下文访�?
可以有任意多个函数上下文,每次调用函数创建一个新的上下文,会创建一个私有作用域,函数内部声明的任何变量都不能在当前函数作用域外部直接访�?

二、生命周�?

执行上下文的生命周期包括三个阶段:创建阶�?�?执行阶段 �?回收阶段

创建阶段

创建阶段即当函数被调用,但未执行任何其内部代码之�?
创建阶段做了三件事:

  • 确定 this 的值,也被称为 This Binding
  • LexicalEnvironment(词法环境) 组件被创�?- VariableEnvironment(变量环境) 组件被创�?
    伪代码如下:
1
2
3
4
5
ExecutionContext = {  
ThisBinding = <this value>, // 确定this
LexicalEnvironment = { ... }, // 词法环境
VariableEnvironment = { ... }, // 变量环境
}

This Binding

确定this的值我们前面讲到,this的值是在执行的时候才能确认,定义的时候不能确�?

词法环境

词法环境有两个组成部分:

  • 全局环境:是一个没有外部环境的词法环境,其外部环境引用为 null,有一个全局对象,this 的值指向这个全局对象

  • 函数环境:用户在函数中定义的变量被存储在环境记录中,包含了arguments 对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境

伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
GlobalExectionContext = {  // 全局执行上下�?  LexicalEnvironment: {       // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Object", // 全局环境
// 标识符绑定在这里
outer: <null> // 对外部环境的引用
}
}

FunctionExectionContext = { // 函数执行上下�? LexicalEnvironment: { // 词法环境
EnvironmentRecord: { // 环境记录
Type: "Declarative", // 函数环境
// 标识符绑定在这里 // 对外部环境的引用
outer: <Global or outer function environment reference>
}
}

变量环境

变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属�?
�?ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量�?let �?const )绑定,而后者仅用于存储变量�?var )绑�?
举个例子

1
2
3
4
5
6
7
8
9
10
let a = 20;  
const b = 30;
var c;

function multiply(e, f) {
var g = 20;
return e * f * g;
}

c = multiply(20, 30);

执行上下文如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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>
}
}

留意上面的代码,letconst定义的变量ab在创建阶段没有被赋值,但var声明的变量从在创建阶段被赋值为undefined

这是因为,创建阶段,会在代码中扫描变量和函数声明,然后将函数声明存储在环境中

但变量会被初始化为undefined(var声明的情况下)和保持uninitialized(未初始化状�?(使用letconst声明的情况下)

这就是变量提升的实际原因

执行阶段

在这阶段,执行变量赋值、代码执�?
如果 Javascript 引擎在源代码中声明的实际位置找不到变量的值,那么将为其分�?undefined �?

回收阶段

执行上下文出栈等待虚拟机回收执行上下�?

二、执行栈

执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文

Javascript引擎开始执行你第一行脚本代码的时候,它就会创建一个全局执行上下文然后将它压到执行栈�?
每当引擎碰到一个函数的时候,它就会创建一个函数执行上下文,然后将这个执行上下文压到执行栈�?
引擎会执行位于执行栈栈顶的执行上下文(一般是函数执行上下�?,当该函数执行结束后,对应的执行上下文就会被弹出,然后控制流程到达执行栈的下一个执行上下文

举个例子�?

1
2
3
4
5
6
7
8
9
10
11
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)来进行分片上传

如下�?

上传完之后再由服务端对所有上传的文件进行汇总整合成原始的文�?
大致流程如下�?1. 将需要上传的文件按照一定的分割规则,分割成相同大小的数据块�?2. 初始化一个分片上传任务,返回本次分片上传唯一标识�?3. 按照一定的策略(串行或并行)发送各个分片数据块�?4. 发送完成后,服务端根据判断数据上传是否完整,如果完整,则进行数据块合成得到原始文件

断点续传

断点续传指的是在下载或上传时,将下载或上传任务人为的划分为几个部�?
每一个部分采用一个线程进行上传或下载,如果碰到网络故障,可以从已经上传或下载的部分开始继续上传下载未完成的部分,而没有必要从头开始上传下载。用户可以节省时间,提高速度

一般实现方式有两种�?

  • 服务器端返回,告知从哪开�?- 浏览器端自行处理

上传过程中将文件在服务器写为临时文件,等全部写完了(文件上传完),将此临时文件重命名为正式文件即�?
如果中途上传中断过,下次上传的时候根据当前临时文件大小,作为在客户端读取文件的偏移量,从此位置继续读取文件数据块,上传到服务器从此偏移量继续写入文件即可

二、实现思路

整体思路比较简单,拿到文件,保存文件唯一性标识,切割文件,分段上传,每次上传一段,根据唯一性标识判断文件上传进度,直到文件的全部片段上传完�?

下面的内容都是伪代码

读取文件内容�?

1
2
3
4
const input = document.querySelector('input');
input.addEventListener('change', function() {
var file = this.files[0];
});

可以使用md5实现文件的唯一�?

1
const md5code = md5(file);

然后开始对文件进行分割

1
2
3
4
5
6
var reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.addEventListener("load", function(e) {
//�?0M切割一�?这里只做一个切割演示,实际切割需要循环切割,
var slice = e.target.result.slice(0, 10*1024*1024);
});

h5上传一个(一片)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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) {
//进度�? }
}

这里给出常见的图片和视频的文件类型判�?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
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);
}
}

调用方法如下

1
2
3
4
checkFileType('(mov|mp4|avi)',file,function(fileType){
// fileType = mp4,
// 如果file的类型不在枚举之列,则返回false
});

上面上传文件的一步,可以改成�?

1
formdata.append('filename', md5code+'.'+fileType);

有了切割上传后,也就有了文件唯一标识信息,断点续传变成了后台的一个小小的逻辑判断

后端主要做的内容为:根据前端传给后台的md5值,到服务器磁盘查找是否有之前未完成的文件合并信息(也就是未完成的半成品文件切片),取到之后根据上传切片的数量,返回数据告诉前端开始从第几节上�?
如果想要暂停切片的上传,可以使用XMLHttpRequest �?abort 方法

三、使用场�?

  • 大文件加速上传:当文件大小超过预期大小时,使用分片上传可实现并行上传多个 Part�?以加快上传速度
  • 网络环境较差:建议使用分片上传。当出现上传失败的时候,仅需重传失败的Part
  • 流式上传:可以在需要上传的文件大小还不确定的情况下开始上传。这种场景在视频监控等行业应用中比较常见

小结

当前的伪代码,只是提供一个简单的思路,想要把事情做到极致,我们还需要考虑到更多场景,比如

  • 切片上传失败怎么�?- 上传过程中刷新页面怎么�?- 如何进行并行上传
  • 切片什么时候按数量切,什么时候按大小�?- 如何结合 Web Worker 处理大文件上�?- 如何实现秒传

人生又何尝不是如此,极致的人生体验有无限可能,越是后面才发现越是精彩 _

参考文�?

面试官:深拷贝浅拷贝的区别?如何实现一个深拷贝�?

一、数据类型存�?

前面文章我们讲到,JavaScript中存在两大数据类型:

  • 基本类型
  • 引用类型

基本类型数据保存在在栈内存中

引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈�?

二、浅拷贝

浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷�?
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址

即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址

下面简单实现一个浅拷贝

1
2
3
4
5
6
7
8
9
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()
  • 使用拓展运算符实现的复制

Object.assign

1
2
3
4
5
6
7
8
9
10
11
12
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);

slice()

1
2
3
4
5
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"]

concat()

1
2
3
4
5
const fxArr = ["One", "Two", "Three"]
const fxArrs = fxArr.concat()
fxArrs[1] = "love";
console.log(fxArr) // ["One", "Two", "Three"]
console.log(fxArrs) // ["One", "love", "Three"]

拓展运算�?

1
2
3
4
5
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()

  • 手写循环递归

_.cloneDeep()

1
2
3
4
5
6
7
8
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

jQuery.extend()

1
2
3
4
5
6
7
8
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

JSON.stringify()

1
const obj2=JSON.parse(JSON.stringify(obj1));

但是这种方式存在弊端,会忽略undefinedsymbol函数

1
2
3
4
5
6
7
8
const obj = {
name: 'A',
name1: undefined,
name3: function() {},
name4: Symbol('A')
}
const obj2 = JSON.parse(JSON.stringify(obj));
console.log(obj2); // {name: "A"}

循环递归

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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;
}

四、区�?

下面首先借助两张图,可以更加清晰看到浅拷贝与深拷贝的区别

从上图发现,浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一�?
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对�?

1
2
3
4
5
6
7
8
9
// 浅拷�?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 ] }

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象

1
2
3
4
5
6
7
8
9
10
// 深拷�?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 ] }

小结

前提为拷贝类型为引用类型的情况下�?

  • 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址

  • 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址

0%