js位置
可视区域的宽度和高度
1.clientHeight :可视区域的高度,不包括border和滚动条
document.documentElement.clientHeight
document.body.clientHeight // 该可视区域的高度包括body标签中的滚动部分
2.clientWidth: 可视区域的宽度,不包括border和滚动条
document.documentElement.clientWidth
document.body.clientWidth // 该可视区域的宽度包括body标签中的滚动部分
3.offsetHeight: 可视区域的高度,包括border和滚动条
document.documentElement.offsetHeight = document.body.offsetHeight
4.offsetWidth: 可视区域的宽度,包括border和滚动条
document.documentElement.offsetWidth = document.body.offsetWidth
5.scrollHeight: 表示所有区域的高度,包含了因为滚动被隐藏的部分
document.documentElement.scrollHeight = document.body.scrollHeight
6.scrollWidth: 表示所有区域的宽度,包含了因为滚动被隐藏的部分
document.documentElement.scrollWidth = document.body.scrollWidth
7.scrollTop: 纵向滚动的距离
// 如果html文件中有<!DOCTYPE html>那么适用document.documentElement.scrollTop 否则document.body.scrollTop
document.documentElement.scrollTop || document.body.scrollTop
元素的宽度和高度
1.offsetTop: 距离定位父级顶部的距离
document.getElementById('img').offsetTop
2.offsetLeft: 距离定位父级左边的距离
3.offsetWidth: 当前元素的可视宽度
4.offsetHeight: 当前元素的可视高度
5.clientWidth: 当前元素可视区域宽度width + padding
document.getELementById('img').offsetWidth = document.getELementById('img').clientWidth
6.clientHeight: 当前元素可视区域高度height + padding
document.getELementById('img').offsetHeight = document.getELementById('img').clientHeight
7.scrollTop: 当前元素纵向滚动条滚动的距离
8.scrollLeft: 当前元素横向滚动条滚动的距离
9.scrollWidth: 内容宽度
10. scrollHeight: 内容高度
js实现懒加载和预加载
// 全屏幕的懒加载 懒加载也叫延迟加载,指的是在长网页中延迟加载图像,是一种很好优化网页性能的方式。用户滚动到它们之前,可视区域外的图像不会加载。这与图像预加载相反,在长网页上使用延迟加载将使网页加载更快。在某些情况下,它还可以帮助减少服务器负载。常适用图片很多,页面很长的电商网站场景中。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<style>
* {
margin: 0;
padding: 0;
}
img {
display: block;
width: 500px;
height: 500px;
padding: 20px;
}
</style>
</head>
<body>
<div class="img-lazy-load">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
</div>
<script>
const num = document.getElementsByTagName('img').length
let img = document.getElementsByTagName('img')
let n = 0;
lazyLoad();
window.addEventListener('scroll', () => {
lazyLoad()
})
function lazyLoad() {
const clientHeight = document.documentElement.clientHeight
const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop
console.log('clientHeight', clientHeight)
console.log('scrollHeight', scrollHeight)
console.log('img', img[1].scrollHeight)
setTimeout(() => {
for(let i = n ;i <num; i++) {
if(img[i].offsetTop < (clientHeight + scrollHeight)) {
if(img[i].getAttribute('src') === './assets/img/zanwutupian.png') {
img[i].src = img[i].getAttribute('data-src')
}
n = i + 1
}
}
}, 1000)
}
</script>
</body>
</html>
// 某个区域的懒加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<style>
* {
margin: 0;
padding: 0;
}
img {
display: block;
width: 500px;
height: 500px;
}
.img-lazy-load {
width: 1000px;
height: 500px;
border: 1px solid red;
overflow-y: scroll;
}
</style>
</head>
<body>
<div class="img-lazy-load">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
</div>
<script>
const num = document.getElementsByTagName('img').length
let img = document.getElementsByTagName('img')
let n = 0;
lazyLoad();
document.getElementsByClassName('img-lazy-load')[0].addEventListener('scroll', () => {
lazyLoad()
})
function lazyLoad() {
const scrollHeight = document.getElementsByClassName('img-lazy-load')[0].scrollTop
setTimeout(() => {
for(let i = n ;i <num; i++) {
if(img[i].offsetTop <= (500 + scrollHeight)) {
if(img[i].getAttribute('src') === './assets/img/zanwutupian.png') {
img[i].src = img[i].getAttribute('data-src')
}
n = i + 1
}
}
}, 1000)
}
</script>
</body>
</html>
// 预加载
资源预加载是另一个性能优化技术,我们可以使用该技术来预先告知浏览器某些资源可能在将来会被使用到。预加载简单来说就是将所有所需的资源提前请求加载到本地,这样后面在需要用到时就直接从缓存取资源。
preload提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。提供的好处主要是
1.将加载和执行分离开,可不阻塞渲染和document的onload事件
2.提前加载制定资源,不再出现依赖的font字体隔了一段时间才刷出
如何使用preload
使用link标签创建
<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style">
<!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/path/to/style.css';
document.head.appendChild(link);
</script>
// 预加载css
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="preload" href="index.css" as="style"/>
<link rel="stylesheet" href="index.css"/>
</head>
<body>
</body>
</html>
// 预加载js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link rel="preload" href="main.js" as="script"/>
</head>
<body>
<h1>hello world</h1>
<script src="main.js"></srcipt>
</body>
</html>
// 图片预加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Title</title>
<style>
img {
width: 100px;
height: 100px;
}
.img-lazy-load {
width: 500px;
height: 500px;
border: 1px solid red;
}
</style>
</head>
<body>
<div class="img-lazy-load">
<img src="./assets/img/zanwutupian.png" data-src="http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg" alt="">
</div>
<script>
let count = 0
let imgs = [
'http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg',
'http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg',
'http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg',
'http://ww1.sinaimg.cn/large/006y8mN6gw1fa7kaed2hpj30sg0l9q54.jpg',
'http://ww4.sinaimg.cn/large/006y8mN6gw1fa5obmqrmvj305k05k3yh.jpg'
]
preLoadImg(imgs[0])
function preLoadImg(src) {
const img = document.createElement('img')
img.src = src
img.addEventListener('load',() => {
console.log('加载完成')
})
img.addEventListener('error', () => {
console.log('加载失败')
})
}
document.getElementsByClassName('img-lazy-load')[0].addEventListener('click', ()=> {
if(count < (imgs.length - 1)) {
document.getElementsByTagName('img')[0].src = imgs[count]
count = count + 1
preLoadImg(imgs[count])
} else {
console.log('全部已加载完毕')
}
})
</script>
</body>
</html>
事件委托/代理是什么?
本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到 目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。 使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
事件传播?
事件传播有三个阶段: 1.捕获阶段–事件从 window 开始,然后向下到每个元素,直到到达目标元素事件或event.target。 2.目标阶段–事件已达到目标元素。 3.冒泡阶段–事件从目标元素冒泡,然后上升到每个元素,直到到达 window。
DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?
1.创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
2.添加、移除、替换、插入
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
3.查找
getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();
4.属性操作
getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);
js数组和字符串有哪些原生方法,列举一下
1.push
Array.prototype.myPush = function (...value) {
console.log(value)
for(let i = 0; i < value.length; i++) {
this[this.length] = value[i]
}
return this.length
}
2.unshift
Array.prototype.myUnshift = function (...value) {
const allValue = [...value, ...this]
for(let i = 0; i < allValue.length; i++) {
this[i] = allValue[i]
}
return this.length
}
3.pop
Array.prototype.myPop = function () {
if(!this.length) return undefined
const endData = this[this.length -1]
this[this.length -1] = null
this.length = this.length -1
return endData
}
4.shift
Array.prototype.myShift = function () {
if(!this.length) return undefined
const startData = this[0]
for(let i = 0; i < this.length; i++) {
this[i] = this[i+1]
}
this.length = this.length - 1
return startData
}
5.splice
// 返回的是被删除的元素数组,也会改变原数组
6.concat
7.forEach
forEach会改变原数组
let list = [1, 2, 3, 4, 5]
list.forEach((item, index, arr) => {
arr[index] = item *2
})
console.log('---', list) // [2,4,6,8,10]
Array.prototype.myForEach = function (callback, context) {
//不能是null调用方法
if(this === null) {
throw new Error('can not null')
}
if(typeof callback !== 'function') {
throw new Error('callback is not function')
}
let arr = Array.prototype.slice.call(this)
let len = arr.length;
console.log('------context', context, this)
for(let i = 0; i<len; i++) {
callback.call(context, arr[i], i, arr) //context改变this的指向
}
return arr;
}
8.reduce
Array.prototype.myReduce = function(callback, initialValue) {
// 不能是null调用方法
if (this === null) {
throw new TypeError(
"Array.prototype.reduce" + "called on null or undefined"
);
}
// 第一个参数必须要为function
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
let _len = arr.length;
// 结果数组
let res = [];
// 开始的数组索引 默认为 0
let startIndex = 0;
// 判断是否为有初始化值 initalValue
if (initialValue === undefined) {
res = this[0]
} else {
res = initialValue;
}
// 在上一步拿到初始值,循环调用传入的回调函数,并且过滤松散值
for (let i = startIndex++; i < this.length; i++) {
res = callback.call(null, res, this[i], i, this);
}
return res;
};
const arr = ['',100,101,102]
const a = arr.myReduce((pre, cur, index, arr) => {
return pre = pre + cur
})
console.log('-------', a) // 100101102
const arr = [null,100,101,102]
const a = arr.myReduce((pre, cur, index, arr) => {
return pre = pre + cur
})
console.log('-------', a) // 303
const arr = [undefined,100,101,102]
const a = arr.myReduce((pre, cur, index, arr) => {
return pre = pre + cur
})
console.log('-------', a) // NaN
9.filter
Array.prototype.myFilter = function(callback, context) {
// 不能是null调用方法
if (this === null) {
throw new TypeError(
"Array.prototype.reduce" + "called on null or undefined"
);
}
// 第一个参数必须要为function
if (typeof callback !== "function") {
throw new TypeError(callback + " is not a function");
}
// 获取数组
let aArr = Array.prototype.slice.call(this);
let _len = aArr.length;
let aFArr = [];
// 循环调用callback
for (let i = 0; i < _len; i++) {
if (!aArr.hasOwnProperty(i)) {
continue;
}
callback.call(context, aArr[i], i, this) && aFArr.push(aArr[i]); //如果返回false,就不会执行后面这条语句了,返回true,就会执行后面这条语句了
}
return aFArr;
};
js跳出forEach循环的骚姿势
通过使用try catch跳出forEach循环, break和continue在forEach上是不能使用的。
try {
const array = [1,2,3,4,5,6]
array.forEach((value) => {
console.log('---', value)
if(value > 3) {
throw new Error('抛出异常跳出')
}
})
} catch(e) {
console.log('e', e)
}
跳出for循环(没有return方法)
for(let i = 0; i<=10; i++) {
if(i === 8) {
break;
}
console.log('------', i)
}
continue 跳出当前循环,进入新的循环
for(let i=1; i<=10; i++) {
if(i === 8) {
continue;
}
console.log(i)
}
跳出for in 循环(使用break, 不能return)
let arr = [1,2,3,4,5]
for(let key in arr) {
if(key > 3) {
break
}
console.log(arr[i]) // 1,2,3,4
}
every: 用于检测数组中所有元素是否符合指定条件。(全部)
说明:
如果数组中检测到有一个元素不满足,则整个表达式返回 false ,剩下的元素不会再进行检测,如果所有元素都满足条件,则返回 true 。
array.every(function(value, index, arr))
const arr = [1,2,3,4,5]
const a = arr.every((value, index) => {
if(value > 3) {
return false
} else {
console.log('value', value, index)
return true
}
})
console.log('a', a) // false
some: 用于检测数组中是否有元素符合指定条件。(部分)
如果数组中检测到有一个元素满足,则整个表达式返回 true ,剩下的元素不会再进行检测,如果没有元素都满足条件,则返回 false 。
array.some(function(value, index, arr))
let list = [1, 2, 3, 4, 5]
list.some((value, index) => {
if(value ===3) {
return true
}
console.log(value)
})
for of 跳出循环,使用break
let arr = [1,2,3,4,5]
for(val of arr) {
if(val > 3) { break;}
console.log('===', value) // 1,2,3
}
arguments 的对象是什么?
arguments对象是函数中传递的参数值的集合。它是一个类似数组的对象,因为它有一个length属性,我们可以使用数组索引表示法arguments[1]来访问单个值,但它没有数组中的内置方法,如:forEach、reduce、filter和map。 我们可以使用Array.prototype.slice将arguments对象转换成一个数组。
function one() {
return Array.prototype.slice.call(arguments);
}
当我们调用函数four时,它会抛出一个ReferenceError: arguments is not defined error。使用rest语法,可以解决这个问题。
数组扁平化?
const arr = [1, [2, [3, [4, 5]]], 6];
方法一:使用flat()
const res = arr.flat()
方法二:利用正则
const res = JSON.stringify(arr).replace(/\[|\]/g, '').split(',')
方法三:使用reduce
const flatten = arr => { return arr.reduce((pre, cur) => { return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); }, []) } const res4 = flatten(arr);
数组去重?
const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}];
方法一:利用Set
const res1 = Array.from(new Set(arr));
方法二:利用indexOf
const unique2 = arr => {
const res = [];
for (let i = 0; i < arr.length; i++) {
if (res.indexOf(arr[i]) === -1) res.push(arr[i]);
}
return res;
}
方法三:利用includes
let result = []
const noRepeat = () => {
for(let i = 0; i<arr.length; i++) {
if(!result.includes(arr[i])) {
result.push(arr[i])
}
}
return result
}
类数组转化为数组?
类数组:
书面有这么一句定义来阐述类数组对象:只包含使用从零开始,且自然递增的整数做键名,并且定义了length表示元素个数的对象,我们就认为它是类数组对象。类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。
var array = ['zhangsan', 'lisi', 'zhaoliu'];
var arrayLike = { 0: 'zhangsan', 1: 'lisi', 2: 'zhaoliu', length: 3 }
方法一:Array.from Array.from()方法就是将一个类数组对象或者可遍历对象转换成一个真正的数组
Array.from(document.querySelectorAll('div'))
方法二:Array.prototype.slice.call()
Array.prototype.slice.call(document.querySelectorAll('div'))
方法三:扩展运算符
[...document.querySelectorAll('div')]
方法四:利用concat
Array.prototype.concat.apply([], document.querySelectorAll('div'));
Array.prototype.map()?
Array.prototype.forEach()?
Array.prototype.forEach = (callback, args) => {
if(type callback !== 'function') {
throw new Error('')
}
const arr = this
const len = arr.length
let count = 0
while(count < len) {
if(count in arr) {
callback(args, arr[count], count, arr)
}
count ++
}
}
js节流和防抖?
防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。搜索框和window.resize。
debounce(fn, time) {
let timer = null
return function (args) {
let that = this
let _args = args
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(that, _args)
}, time)
}
}
节流:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。节流常应用于鼠标不断点击触发、监听滚动事件。
const throttle = (fn, time) => {
let flag = true;
return function() {
if (!flag) return; flag = false;
setTimeout(() => {
fn.apply(this, arguments); flag = true;
}, time);
}
}
函数珂里化
function add() {
const _args = [...arguments];
function fn() { _args.push(...arguments); return fn; }
fn.toString = function() { return _args.reduce((sum, cur) => sum + cur); }
return fn;
}
AJAX
我对 ajax 的理解是,它是一种异步通信的方法,通过直接由 js 脚本向服务器发起 http 通信,然后根据服务器返回的数据,更新网页的相应部分,而不用刷新整个页面的一种方法。
const getJSON = function(url) {
return new Promise((resolve, reject) => {
const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp')
xhr.open('GET', url, false)
xhr.setRequestHeader('Accept', 'application/json')
xhr.onreadystatechange = function() {
if(xhr.readystatus !== 4) return
if(xhr.status === 200 || xhr.status===304) {
resolve(xhr.responseText)
} esle {
reject(new Error(xhr.responseText))
}
}
xhr.send()
})
}
渲染几万条数据不卡住页面
1.requestAnimationFrame
window.requestAnimationFrame()
告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行。
2.执行频率
这时候有小伙伴就要问了,我没有像setTimeout和setInterval那样设置时间,它为什么会间隔执行呢?
回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。
屏幕刷新频率(次数): 屏幕每秒出现图像的次数。普通笔记本为60Hz。(1/60s出现1次)
在同一个帧中的多个回调函数 ,它们每一个都会接受到一个相同的时间戳 ,即使在计算上一个回调函数的工作负载期间已经消耗了一些时间。该时间戳是一个十进制数,单位毫秒,最小精度为1ms(1000μs)。
(() => {
function test1(timestamp) {
console.log(`🚀🚀hello ~ requestAnimationFrame1 ${timestamp}`); requestAnimationFrame(test1)
}
function test2(timestamp) {
console.log(`🚀🚀hello ~ requestAnimationFrame2 ${timestamp}`); requestAnimationFrame(test2) }
requestAnimationFrame(test1)
requestAnimationFrame(test2)
})()
可以看到,两个 requestAnimationFrame 在控制台输出的时间戳是一样的。也就是浏览器刷新一次的时候,执行所有的 requestAnimationFrame ,并且它们的回调参数是一模一样的。
3.浏览器的自我拯救 为了提高性能和电池寿命,因此在大多数浏览器里,当requestAnimationFrame()运行在后台标签页(即你去看其他页面的时候,当前这个页面就会暂停)或者隐藏的里时,requestAnimationFrame()会被暂停调用以提升性能和电池寿命。 4.requestAnimationFrame的返回值 requestAnimationFrame的返回值,返回的是执行的次数。可以看见,每执行一次,数值就会+1
(() => {
const beginBtn = document.querySelector("#begin")
let myRef;
beginBtn.addEventListener("click", () => {
myRef = requestAnimationFrame(test) })
function test() {
myRef = requestAnimationFrame(test)
console.log('🚀🚀~ myRef:', myRef);
} })()
- 终止执行 cancelAnimationFrame 那如果我想要在特定的条件下终止requestAnimationFrame 怎么办呢,官方也给出了答案,那就是 cancelAnimationFrame API。 只需要把requestAnimationFrame的返回值作为参数传递给cancelAnimationFrame就可以了。
(() => {
const beginBtn = document.querySelector("#begin")
const endBtn = document.querySelector("#end")
let myRef;
beginBtn.addEventListener("click", () => {
myRef = requestAnimationFrame(test)
})
endBtn.addEventListener("click", () => {
cancelAnimationFrame(myRef) //参数必填
})
function test() {
myRef = requestAnimationFrame(test)
}
})()
其实也可以直接用简单的if语句也可以达到终止的效果
6.setTimeout && setInterval
setTimeout和setInterval的问题是,它们不够精确。它们的内在运行机制决定了时间间隔参数实际上只是指定了把动画代码添加到浏览器UI线程队列中以等待执行的时间。如果队列前面已经加入了其它任务,那动画代码就要等前面的任务完成后再执行,并且如果时间间隔过短(小于16.7ms)会造成丢帧,所以就会导致动画可能不会按照预设的去执行,降低用户体验。
requestAnimationFrame采用浏览器时间间隔,保持最佳绘制效率,不会因为间隔时间过短,造成过度绘制,消耗性能;也不会因为间隔时间太长,使用动画卡顿不流畅,让各种网页动画效果能够有一个统一的刷新机制,从而节省系统资源,提高系统性能,改善视觉效果。
css3动画
css3的transition和animation搭配使用可以说是非常强大了,但是也有的触手伸不到的地方,比如scrollTop,另外CSS3动画支持的贝塞尔曲线也是有限的。
渲染大数据时,合理使用createDocumentFragment和requestAnimationFrame,将操作切分为一小段一小段执行。
setTimeout(() => {
// 插入十万条数据
const total = 100000;
// 一次插入的数据
const once = 20;
// 插入数据需要的次数
const loopCount = Math.ceil(total / once);
let countOfRender = 0;
const ul = document.querySelector('ul');
// 添加数据的方法
function add() {
const fragment = document.createDocumentFragment();
for(let i = 0; i < once; i++) {
const li = document.createElement('li');
li.innerText = Math.floor(Math.random() * total);
fragment.appendChild(li);
}
ul.appendChild(fragment);
countOfRender += 1;
loop();
}
function loop() {
if(countOfRender < loopCount) {
window.requestAnimationFrame(add);
}
}
loop();
}, 0)
createDocumentFragment方法: 在更新少量节点的时候可以直接向document.body节点中添加,但是当要向document中添加大量数据时,如果直接添加这些新节点,这个过程非常缓慢,因为每添加一个节点都会调用父节点的appendChild()方法,在性能方面也不会很好。为了解决这个问题,可以创建一个文档碎片,把所有的新节点附加其上,然后把文档碎片一次性添加到document中。
SDK 和 API 的区别是什么?
API: 研发人员A开发了软件A,研发人员B正在研发软件B。 有一天,研发人员B想要调用软件A的部分功能来用,但是他又不想从头看一遍软件A的源码和功能实现过程,怎么办呢?研发人员A想了一个好主意:我把软件A里你需要的功能打包好,写成一个函数。你按照我说的流程,把这个函数放在软件B里,就能直接用我的功能了!其中,API就是研发人员A说的那个函数。 SDK: SDK 就是 Software Development Kit 的缩写,翻译过来——软件开发工具包。这是一个覆盖面相当广泛的名词,可以这么说:辅助开发某一类软件的相关文档、范例和工具的集合都可以叫做SDK。 SDK被开发出来是为了减少程序员工作量的。 比如:有公司开发出某种软件的某一功能,把它封装成SDK(比如数据分析SDK就是能够实现数据分析功能的SDK),出售给其他公司做开发用,其他公司如果想要给软件开发出某种功能,但又不想从头开始搞开发,直接付钱省事。
JSONP
script标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于GET请求
const jsonp = ({ url, params, callbackName }) => {
const generateUrl = () => {
let dataSrc = '';
for (let key in params) {
if (Object.prototype.hasOwnProperty.call(params, key)) {
dataSrc += `${key}=${params[key]}&`;
}
}
dataSrc += `callback=${callbackName}`;
return `${url}?${dataSrc}`;
}
return new Promise((resolve, reject) => {
const scriptEle = document.createElement('script');
scriptEle.src = generateUrl();
document.body.appendChild(scriptEle);
window[callbackName] = data => {
resolve(data);
document.removeChild(scriptEle);
}
})
}
mouseover和mouseenter的区别
1.mouseover: 当鼠标移入元素或其子元素都会触发事件,所以有一个重复触发,冒泡的过程。对应的移除事件是mouseout
2.mouseenter: 当鼠标移入元素本身(不包括元素的子元素)会触发事件,也就是不会冒泡,对应的移除事件是mouseleave
js原型与原型链
我们先来了解下面引用类型的四个规则:
1、引用类型,都具有对象特性,即可自由扩展属性。
2、引用类型,都有一个隐式原型 __proto__ 属性,属性值是一个普通的对象。
3、引用类型,隐式原型 __proto__ 的属性值指向它的构造函数的显式原型 prototype 属性值。
4、当你试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么它会去它的隐式原型 __proto__(也就是它的构造函数的显式原型 prototype)中寻找。
引用类型:Object、Array、Function、Date、RegExp。这里我姑且称 proto 为隐式原型,没有官方中文叫法,大家都瞎叫居多。
规则一:
引用类型,都具有对象特性,即可自由扩展属性:
const obj = {}
const arr = []
const fn = function () {}
obj.a = 1
arr.a = 1
fn.a = 1
console.log(obj.a) // 1
console.log(arr.a) // 1
console.log(fn.a) // 1
console.log('obj.__proto__ === Object.prototype', obj.__proto__ === Object.prototype) //true
console.log(obj.prototype) // undefiend
console.log(fn.prototype.__proto__ === Object.prototype) // true
console.log(fn.prototype.__proto__ === Object.__proto__) //false
console.log(Object.prototype === Object.__proto__) // false
console.log(Object.__proto__.__proto__ === Object.prototype) // true ,谨记
规则二: 引用类型,都有一个隐式原型__proto__属性,属性值是一个普通的对象
const obj = {};
const arr = [];
const fn = function() {}
console.log('obj.__proto__', obj.__proto__);
console.log('arr.__proto__', arr.__proto__);
console.log('fn.__proto__', fn.__proto__);
规则三: 引用类型,隐式原型__proto__的属性值指向它的构造函数的显示原型prototype属性值
const obj = {};
const arr = [];
const fn = function() {}
obj.__proto__ === Object.prototype // true
arr.__proto__ === Array.prototype // true
fn.__proto__ === Function.prototype // true
规则四:
instanceof 运算符用于测试构造函数的 prototype 属性是否出现在对象原型链中的任何位置。 instanceof 的简易手写版
// 变量R的原型 存在于 变量L的原型链上
function instance_of (L, R) {
// 验证如果为基本数据类型,就直接返回 false
const baseType = ['string', 'number', 'boolean', 'undefined', 'symbol']
if(baseType.includes(typeof(L))) { return false }
let RP = R.prototype; // 取 R 的显示原型
L = L.__proto__; // 取 L 的隐式原型
while (true) {
if (L === null) { // 找到最顶层
return false;
}
if (L === RP) { // 严格相等
return true;
}
L = L.__proto__; // 没找到继续向上一层原型链查找
}
}
规则五:对象原型__proto__ 对象都会有一个__proto__属性,指向构造函数的prototype原型对象上,之所以对象可以使用构造函数的prototype原型对象上的属性和方法,就是因为有__proto__原型的存在。
function Star(name, age) {
this.name = name,
this.age = age,
this.play = function() {
console.log('我打球贼厉害')
}
}
var James = new Star('詹姆斯', 37);
console.log(James)
console.log(James.__proto__ === Start.prototype)
__proto__和prototype的区别
1.prototype是构造函数特有的属性,它的值是一个对象,这个对象包含的是构造函数想要共享的属性和方法。
2.proto,引用类型都有一个__proto__属性。
function Foo() {
this.a = '123'
this.b = '456'
}
const a = new Foo()
console.log('-------',a)
console.log('------1', a.__proto__ === Foo.prototype) // true
console.log('------2',Foo.prototype.constructor === Foo) // true
console.log('------3', Foo.prototype.__proto__ === Object.prototype) // true
console.log('------4', Object.prototype.__proto__ === null) // true
console.log('-------5', Foo.__proto__ === Function.prototype) // true
console.log('-------6', Object.__proto__ === Function.prototype) // true
console.log('-------7', Function.prototype.__proto__ === Object.prototype) // true
console.log('-------8', Function.prototype.constructor === Function) // true
console.log('-------9', Object.prototype.constructor === Object) // true
js的数据类型
javascript一共有8种数据类型,其中有7种基本数据类型:undefined、null、boolean、number、string、symbol和bigInt(es10) 引用类型:Object 原始值和引用值区别: 1.动态属性,原始值不能随便加动态属性,但是引用值则可以随便添加、删除、修改。 2.赋值效果不同 原始值直接存储在栈中,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。原始值赋值,两个变量是独立的,没有任何关联。
const a = 1
const b = a
引用值同时存储在栈和堆中,占据空间大、大小不固定。引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值,会首先检索其栈中的地址,取得地址后从堆中获得实体。引用值赋值,两个变量会同时指向堆中的同一个对象。
let obj1 = new Object()
let obj2 = obj1
obj1.name = 'Nicholas'
console.log(obj2.name) // 'Nicholas'
JS中数据类型的判断( typeof,instanceof,constructor,Object.prototype.toString.call())
1.typeof
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof 'str'); // string
console.log(typeof []); // object []数组的数据类型在 typeof 中被解释为 object
console.log(typeof function(){}); // function
console.log(typeof {}); // object
console.log(typeof undefined); // undefined
console.log(typeof null); // object null 的数据类型被 typeof 解释为 object
typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型,所以想判断一个对象的正确类型,这时候可以考虑使用 instanceof 2.instanceof instanceof 可以正确的判断对象的类型,用来测试一个对象在其原型链中是否存在一个构造函数的 prototype属性。
console.log(2 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log('str' instanceof String); // false
console.log([] instanceof Array); // true
console.log(function(){} instanceof Function); // true
console.log({} instanceof Object); // true
// console.log(undefined instanceof undefined); // 报错
// console.log(null instanceof null); // 报错
3.constructor
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true
这里有一个坑,如果我创建一个对象,更改它的原型,constructor就会变得不可靠了 ==存在疑问
function Fn(){};
Fn.prototype=new Array();
var f=new Fn();
console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
4.Object.prototype.toString.call()使用 Object 对象的原型方法 toString ,使用 call 进行狸猫换太子,借用Object的 toString 方法。
const a = Object.prototype.toString;
console.log(a.call(2)); // ‘[object, Number]’
console.log(a.call(true)); // ‘[object, Boolean]’
console.log(a.call('str')); // ‘[object, String]’
console.log(a.call([])); // ‘[object, Array]’
console.log(a.call(function(){}));// ‘[object, Function]’
console.log(a.call({}));// ‘[object, Object]’
console.log(a.call(undefined));// ‘[object, Undefined]’
console.log(a.call(null)); // ‘[object, Null]’
null和undefined的区别
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。 undefined 代表的含义是未定义, null 代表的含义是空对象(其实不是真的对象,请看下面的注意!)。一般变量声明了但还没有定义的时候会返回 undefined,null 主要用于赋值给一些可能会返回对象的变量,作为初始化。
其实 null 不是对象,虽然 typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象,然而 null 表示为全零,所以将它错误的判断为 object 。虽然现在的内部类型判断代码已经改变了,但是对于这个 Bug 却是一直流传下来。
当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等 号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
{}和[]的valueOf和toString的结果是什么?
Javascript 的作用域和作用域链
作用域:在运行时代码中的某些特定部分中变量,函数和对象的可访问性。
function outFun2() {
var inVariable = "内层变量2";
}
outFun2();//要先执行这个函数,否则根本不知道里面是啥
console.log(inVariable); // Uncaught ReferenceError: inVariable is not defined
从上面的例子可以体会到作用域的概念,变量inVariable在全局作用域没有声明,所以在全局作用域下取值会报错。我们可以这样理解:作用域就是一个独立的地盘,让变量不会外泄、暴露出去。也就是说作用域最大的用处就是隔离变量,不同作用域下同名变量不会有冲突。
ES6 之前 JavaScript 没有块级作用域,只有全局作用域和函数作用域。ES6的到来,为我们提供了‘块级作用域’,可通过新增命令let和const来体现。
全局作用域和函数作用域
在代码中任何地方都能访问到的对象拥有全局作用域,一般来说以下几种情形拥有全局作用域:
1.最外层函数 和在最外层函数外面定义的变量拥有全局作用域
2.所有末定义直接赋值的变量自动声明为拥有全局作用域
3.所有window对象的属性拥有全局作用域
作用域是分层的,内层作用域可以访问外层作用域的变量,反之则不行。
值得注意的是:块语句(大括号“{}”中间的语句),如 if 和 switch 条件语句或 for 和 while 循环语句,不像函数,它们不会创建一个新的作用域。在块语句中定义的变量将保留在它们已经存在的作用域中。
if (true) {
// 'if' 条件语句块不会创建一个新的作用域
var name = 'Hammad'; // name 依然在全局作用域中
}
console.log(name); // logs 'Hammad'
S 的初学者经常需要花点时间才能习惯变量提升,而如果不理解这种特有行为,就可能导致 bug 。正因为如此, ES6 引入了块级作用域,让变量的生命周期更加可控。
块级作用域可通过新增命令let和const声明,所声明的变量在指定块的作用域外无法被访问。块级作用域在如下情况被创建:
- 在一个函数内部
- 在一个代码块(由一对花括号包裹)内部
作用域链
如果父级也没呢?再一层一层向上寻找,直到找到全局作用域还是没找到,就宣布放弃。这种一层一层的关系,就是 作用域链 。
var a = 100
function F1() {
var b = 200
function F2() {
var c = 300
console.log(a) // 自由变量,顺作用域链向父作用域找
console.log(b) // 自由变量,顺作用域链向父作用域找
console.log(c) // 本作用域的变量
}
F2()
}
F1()
谈谈你对this、call、apply和bind的理解
1.在浏览器里,在全局范围内this 指向window对象;
2.在函数中,this永远指向最后调用他的那个对象;
3.构造函数中,this指向new出来的那个新的对象;
4.call、apply、bind中的this被强绑定在指定的那个对象上;
5.箭头函数中this比较特殊,箭头函数this为父作用域的this,不是调用时的this.要知道前四种方式,都是调用时确定,也就是动态的,而箭头函数的this指向是静态的,声明的时候就确定了下来;
6.apply、call、bind都是js给函数内置的一些API,调用他们可以为函数指定this的执行,同时也可以传参。
什么是闭包
闭包是指有权访问另一个函数作用域内变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以 访问到当前函数的局部变量。 闭包有两个常用的用途。 1.闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。 2.函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
function a(){
var n = 0;
function add(){
n++;
console.log(n);
}
return add;
}
var a1 = a(); //注意,函数名只是一个标识(指向函数的指针),而()才是执行函数;
a1(); //1
a1(); //2 第二次调用n变量还在内存中
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。
什么是 DOM 和 BOM?
DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。 BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。BOM 的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局) 对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 locati on 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对 象的子对象。
三种事件模型是什么?
DOM 操作——怎样添加、移除、移动、复制、创建和查找节点?
1.创建新节点
createDocumentFragment() //创建一个DOM片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
2.添加、移除、替换、插入
appendChild(node)
removeChild(node)
replaceChild(new,old)
insertBefore(new,old)
3.查找
getElementById();
getElementsByName();
getElementsByTagName();
getElementsByClassName();
querySelector();
querySelectorAll();
4.属性操作
getAttribute(key);
setAttribute(key, value);
hasAttribute(key);
removeAttribute(key);
JS延迟加载的方式有哪些?
1.将js脚本放在文档的底部,来使js脚本尽可能的在最后来加载执行。
2.给js脚本添加defer属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。(脚本会延迟到整个页面解析完毕后再执行)。
3.给js脚本添加async属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个async属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行,所以脚本文件不能有依赖关系。
4.动态创建DOM标签的方式,我们可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入js脚本。
谈谈你对模块化开发的理解?
1.第一种是 CommonJS 方案,它通过 require 来引入模块,通过 module.exports 定义模块的输出接口。这种模块加载方案是服务器端的解决方案,它是以同步的方式来引入模块的,因为在服务端文件都存储在本地磁盘,所以读取非常快,所以以同步的方式加载没有问题。但如果是在浏览器端,由于模块的加载是使用网络请求,因此使用异步加载的方式更加合适。
2.第二种是 AMD 方案,这种方案采用异步加载的方式来加载模块,模块的加载不影响后面语句的执行,所有依赖这个模块的语句都定义在一个回调函数里,等到加载完成后再执行回调函数。require.js 实现了 AMD 规范。
AMD规范采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。这里介绍用require.js实现AMD规范的模块化:用require.config()指定引用路径等,用define()定义模块,用require()加载模块。
/** 网页中引入require.js及main.js **/
<script src="js/require.js" data-main="js/main"></script>
/** main.js 入口文件/主模块 **/
// 首先用config()指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", //实际路径为js/lib/jquery.min.js
"underscore": "underscore.min",
}
});
// 执行基本操作
require(["jquery","underscore"],function($,_){
// some code here
});
3.第三种是 CMD 方案,这种方案和 AMD 方案都是为了解决异步模块加载的问题,sea.js 实现了 CMD 规范。它和require.js的区别在于模块定义时对依赖的处理不同和对依赖模块的执行时机的处理不同。
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
a.doSomething();
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了
b.doSomething()
}
});
/** CMD写法 **/
define(function(require, exports, module) {
var a = require('./a'); //在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
4.第四种方案是 ES6 提出的方案,使用 import 和 export 的形式来导入导出模块。
ES6模块与CommonJS模块的差异?
1.CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
-
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
-
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令
import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 2.CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 -
运行时加载: CommonJS 模块就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。
-
编译时加载: ES6 模块不是对象,而是通过
export命令显式指定输出的代码,import时采用静态命令的形式。即在import时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。
AMD 和 CMD 规范的区别?
- 第一个方面是在模块定义时对依赖的处理不同。AMD推崇依赖前置,在定义模块的时候就要声明其依赖的模块。而 CMD 推崇就近依赖,只有在用到某个模块的时候再去 require。
- 第二个方面是对依赖模块的执行时机处理不同。首先 AMD 和 CMD 对于模块的加载方式都是异步加载,不过它们的区别在于 模块的执行时机,AMD 在依赖模块加载完成后就直接执行依赖模块,依赖模块的执行顺序和我们书写的顺序不一定一致。而 CMD 在依赖模块加载完成后并不执行,只是下载而已,等到所有的依赖模块都加载好后,进入回调函数逻辑,遇到 require 语句 的时候才执行对应的模块,这样模块的执行顺序就和我们书写的顺序保持一致了。
// CMD
define(function(require, exports, module) {
var a = require("./a");
a.doSomething();
// 此处略去 100 行
var b = require("./b"); // 依赖可以就近书写
b.doSomething();
// ...
});
// AMD 默认推荐
define(["./a", "./b"], function(a, b) {
// 依赖必须一开始就写好
a.doSomething();
// 此处略去 100 行
b.doSomething();
// ...
});
谈谈JS的运行机制?
1.js单线程
JavaScript语言的一大特点就是单线程,即同一时间只能做一件事情。
JavaScript的单线程,与它的用途有关。作为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操作DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?
所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变。
2.js事件循环
js代码执行过程中会有很多任务,这些任务总的分成两类:
- 同步任务
- 异步任务
console.log(1)
setTimeout(function(){
console.log(2)
}, 0)
console.log(3)
同步任务: 同步任务都在主线程上执行,形成一个执行栈
console.log(1)
setTimeout(fn, 0)
console.log(2)
异步任务
JS的异步是通过回调函数实现的
一般而言,异步任务有以下三种类型:
1.普通事件,如click,resize等
2.资源加载,如load,error等
3.定时器,包括setInterval,setTimeout等
异步任务相关回调函数添加到任务队列中(任务队列也称为消息队列)
function() {
console.log(2)
}
js执行机制
1.先执行执行栈中的同步任务
2.异步任务(回调函数)放入任务队列中
3.一旦执行栈中的所有同步任务执行完毕,系统就会按次序读取任务队列中的异步任务,于是被读取的异步任务结束等待状态,进入执行栈,开始执行
4.多个异步任务的时候
当我们打开网站时,网页的渲染过程就是一大堆同步任务,比如页面骨架和页面元素的渲染。而像加载图片音乐之类占用资源大耗时久的任务,就是异步任务。,我们用导图来说明:
-
同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入
Event Table并注册函数。 -
当指定的事情完成时,
Event Table会将这个函数移入Event Queue。 -
主线程内的任务执行完毕为空,会去
Event Queue读取对应的函数,进入主线程执行。 -
上述过程会不断重复,也就是常说的
Event Loop(事件循环)。
那主线程执行栈何时为空呢?js引擎存在monitoring process进程,会持续不断的检查主线程执行栈是否为空,一旦为空,就会去Event Queue那里检查是否有等待被调用的函数。
需要注意的是除了同步任务和异步任务,任务还可以更加细分为macrotask(宏任务)和microtask(微任务),js引擎会优先执行微任务
微任务包括了 promise 的回调、node 中的 process.nextTick 、对 Dom 变化监听的 MutationObserver。
宏任务包括了 script 脚本的执行、setTimeout ,setInterval ,setImmediate 一类的定时事件,还有如 I/O 操作、UI 渲 染等。
异步任务分为宏任务和微任务
setTimeout(function() {
console.log('a')
})
new Promise(function(resolve) {
console.log('b')
for(var i=0; i<10000; i++) {
i=== 99 && resolve()
}
}).then(function() {
console.log('c')
})
console.log('d')
// b
// d
// c
// a
1.首先执行script下的宏任务,遇到setTimeout,将其放入宏任务的队列里。
2.遇到Promise,new Promise直接执行,打印b。
3.遇到then方法,是微任务,将其放到微任务的队列里。
4.遇到console.log('d'),直接打印。
5.本轮宏任务执行完毕,查看微任务,发现then方法里的函数,打印c。
6.本轮event loop全部完成。
7.下一轮循环,先执行宏任务,发现宏任务队列中有一个setTimeout,打印a。
哪些操作会造成内存泄漏?
1.意外的全局变量 2.被遗忘的计时器或回调函数 3.脱离 DOM 的引用 4.闭包
ECMAScript 2015(ES6)有哪些新特性?
1.块作用域 2.类 3.箭头函数 4.模板字符串 5.对象解构 6.Promise
什么是函数式编程? JavaScript的哪些特性使其成为函数式语言的候选语言?
函数式编程(通常缩写为FP)是通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。数式编程是声明式 的而不是命令式 的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。
函数式编程是一种编程范式 ,这意味着它是一种基于一些基本的定义原则(如上所列)思考软件构建的方式。当然,编程范式的其他示例也包括面向对象编程和过程编程。
函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试 - 但如果不熟悉它以及与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。
什么是高阶函数?
高阶函数只是将函数作为参数或返回值的函数。
function higherOrderFunction(param,callback){
return callback(param);
}
js模拟new操作符的实现
1.new操作符会返回一个对象,所以我们需要在内部创建一个对象 2.这个对象,也就是构造函数中的this,可以访问到挂载在this上的任意属性 3.这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数连接起来 4.返回原始值需要忽略,返回对象需要正常处理
function createNew(con, ...args) {
const obj = {}
Object.setPropertyOf(obj, con.property)
let result = con.apply(obj, args)
return result instanceof Object ? result : obj
}
- 首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
- 然后内部创建一个空对象
obj - 因为
obj对象需要访问到构造函数原型链上的属性,所以我们通过setPrototypeOf将两者联系起来。这段代码等同于obj.__proto__ = Con.prototype - 将
obj绑定到构造函数上,并且传入剩余的参数 - 判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用
obj,这样就实现了忽略构造函数返回的原始值 详细参考:juejin.cn/post/684490…
js继承
1.原型链继承
function Parent () {
this.name = 'kevin';
}
Parent.prototype.getName = function () {
console.log(this.name);
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
console.log(child1.getName()) // kevin
缺点: 1.引用类型的属性被所有实例共享
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
}
Child.prototype = new Parent();
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy", "yayu"]
- 在创建child的实例时,不能向Parent传参
2.构造函数继承
function Parent () {
this.names = ['kevin', 'daisy'];
}
function Child () {
Parent.call(this);
}
var child1 = new Child();
child1.names.push('yayu');
console.log(child1.names); // ["kevin", "daisy", "yayu"]
var child2 = new Child();
console.log(child2.names); // ["kevin", "daisy"]
优点: 1.避免了引用类型的属性被所有实例共享 2.可以在Child中向Parent传参
function Parent (name) {
this.name = name;
}
function Child (name) {
Parent.call(this, name);
}
var child1 = new Child('kevin');
console.log(child1.name); // kevin
var child2 = new Child('daisy');
console.log(child2.name); // daisy
缺点:
方法都在构造函数中定义,每次创建实例都会创建一遍方法。
构造继承只能继承父类的实例属性和方法,不能继承原型属性和方法。
3.组合继承
原型继承和构造函数继承组合在一起
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
Child.prototype = new Parent();
var child1 = new Child('kevin', '18');
child1.colors.push('black');
console.log(child1.name); // kevin
console.log(child1.age); // 18
console.log(child1.colors); // ["red", "blue", "green", "black"]
var child2 = new Child('daisy', '20');
console.log(child2.name); // daisy
console.log(child2.age); // 20
console.log(child2.colors); // ["red", "blue", "green"]
优点:融合了原型继承和构造函数的优点,是Javascript中最常用的继承模式
缺点:调用了2次父类构造函数
4.寄生组合式继承
function Parent (name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
Parent.prototype.getName = function () {
console.log(this.name)
}
function Child (name, age) {
Parent.call(this, name);
this.age = age;
}
// 关键的三步
var F = function () {};
F.prototype = Parent.prototype;
Child.prototype = new F();
var child1 = new Child('kevin', '18');
console.log(child1);
优点:解决调用了2次父类构造函数
js设计模式
**1.工厂模式 ** 工程模式类似于现实生活中的工厂可以产生大量相似的产品,去做相同的事情,实现同样的效果,这时候需要使用工厂模式。简单的工厂模式可以理解为解决多个相似问题。
function CreatePerson(name,age,sex) {
var obj = new Object();
obj.name = name;
obj.age = age;
obj.sex = sex;
obj.sayName = function(){
return this.name;
}
return obj;
}
var p1 = new CreatePerson("longen",'28','男');
var p2 = new CreatePerson("tugenhua",'27','女');
console.log(p1.name); // longen
console.log(p1.age); // 28
console.log(p1.sex); // 男
console.log(p1.sayName()); // longen
console.log(p2.name); // tugenhua
console.log(p2.age); // 27
console.log(p2.sex); // 女
console.log(p2.sayName()); // tugenhua
// 返回都是object 无法识别对象的类型 不知道他们是哪个对象的实列
console.log(typeof p1); // object
console.log(typeof p2); // object
console.log(p1 instanceof Object); // true
优点:能解决多个相似的问题。
缺点:不能知道对象识别的问题(对象的类型不知道)。
2.单例模式
// 单体模式
const CreateDiv = function(html) {
console.log(this) //CreateDiv
this.html = html
this.init()
}
CreateDiv.prototype.init = function() {
const div = document.createElement('div')
div.innerHTML = this.html
document.body.appendChild(div)
}
// 代理实现单体模式
const ProxyMode = (function () {
var instance;
return function (html) {
if(!instance) {
instance = new CreateDiv(html)
}
return instance
}
})()
const a = new ProxyMode('aaa')
const b = new ProxyMode('bbb')
console.log(a===b) // true
理解使用单体模式来实现弹窗的基本原理
下面我们继续来使用单体模式来实现一个弹窗的demo;我们先不讨论使用单体模式来实现,我们想下我们平时是怎么编写代码来实现弹窗效果的; 比如我们有一个弹窗,默认的情况下肯定是隐藏的,当我点击的时候,它需要显示出来;如下编写代码:
// 实现弹窗
var createWindow = function(){
var div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild('div');
return div;
};
document.getElementById("Id").onclick = function(){
// 点击后先创建一个div元素
var win = createWindow();
win.style.display = "block";
}
如上的代码;大家可以看看,有明显的缺点,比如我点击一个元素需要创建一个div,我点击第二个元素又会创建一次div,我们频繁的点击某某元素,他们会频繁的创建div的元素,虽然当我们点击关闭的时候可以移除弹出代码,但是呢我们频繁的创建和删除并不好,特别对于性能会有很大的影响,对DOM频繁的操作会引起重绘等,从而影响性能;因此这是非常不好的习惯;我们现在可以使用单体模式来实现弹窗效果,我们只实例化一次就可以了;如下代码:
// 实现单体模式弹窗
var createWindow = (function(){
var div;
return function(){
if(!div) {
div = document.createElement("div");
div.innerHTML = "我是弹窗内容";
div.style.display = 'none';
document.body.appendChild(div);
}
return div;
}
})();
document.getElementById("Id").onclick = function(){
// 点击后先创建一个div元素
var win = createWindow();
win.style.display = "block";
}
3.模块模式
我们通过单体模式理解了是以对象字面量的方式来创建单体模式的,比如如下的对象字面量的方式代码:
const singleMode = {
name: value,
method: function() {}
}
模块模式的思路是为单体模式添加私有变量和私有方法能够减少全局变量的使用;如下就是一个模块模式的代码结构:
var singleMode = (function(){
// 创建私有变量
var privateNum = 112;
// 创建私有函数
function privateFunc(){
// 实现自己的业务逻辑代码
}
// 返回一个对象包含公有方法和属性
return {
publicMethod1: publicMethod1,
publicMethod2: publicMethod1
};
})();
模块模式使用了一个返回对象的匿名函数。在这个匿名函数内部,先定义了私有变量和函数,供内部函数使用,然后将一个对象字面量作为函数的值返回,返回的对象字面量中只包含可以公开的属性和方法。这样的话,可以提供外部使用该方法;由于该返回对象中的公有方法是在匿名函数内部定义的,因此它可以访问内部的私有变量和函数。
使用场景:
如果我们必须创建一个对象并以某些数据进行初始化,同时还要公开一些能够访问这些私有数据的方法,那么我们这个时候就可以使用模块模式了。
4.理解代理模式
本体实例化不直接访问,通过代理的方式进行访问
// 申明一个奶茶妹对象
const teaAndMilkGirl = function (name) {
this.name = name;
}
// ceo先生
const Ceo = function (girl) {
this.girl = girl
this.sendMarriageRing = function (ring) {
console.log('hi'+this.girl.name+'ceo'+ring)
}
}
// 代理
const Proxy = function (girl) {
this.girl = girl
this.sendGirl =function (gift) {
(new Ceo(this.girl)).sendMarriageRing(gift)
}
}
// 初始化
const proxy = new Proxy(new teaAndMilkGirl('123'))
proxy.sendGirl('ring')
5.命令模式
6.策略模式
7.观察者模式
当对象之间存在一对多的依赖关系时,其中一个对象的状态发生改变,所有依赖它的对象都会收到通知,这就是观察者模式。
目标对象Subject:
1.维护观察者列表observerList ---维护拥有订阅权限的弟子列表
2.定义添加观察者的方法 --- 提供弟子购买订阅权限的功能
3.当自身发生变化后,通过调用自己的 notify 方法依次通知每个观察者执行 update 方法 ———— 发布对应任务后通知有订阅权限的弟子。
4.观察者 Observer 需要实现 update 方法,供目标对象调用。update方法中可以执行自定义的业务逻辑 ———— 弟子们需要定义接收任务通知后的方法,例如去抢任务或任务不适合,继续等待下一个任务。
class Observer {
constructor(name) {
this.name = name
}
update({taskType, taskInfo}) {
if(taskType === 'route') {
console.log(`${this.name}不需要日常任务`)
return
}
this.goToTaskHome(taskInfo)
}
goToTaskHome(info) {
console.log(`${this.name}去任务${info}任务`)
}
}
class Subject {
constructor() {
this.observerList = []
}
addObserver(observer) {
this.observerList.push(observer)
}
notify(task) {
console.log('发布五星任务')
this.observerList.forEach(observer => observer.update(task))
}
}
const subject = new Subject()
const stu1 = new Observer('弟子1')
const stu2 = new Observer('弟子2')
// stu1 stu2购买五星任务通知权限
subject.addObserver(stu1)
subject.addObserver(stu2)
// 任务殿发布五星战斗任务
const warTask = {
taskType: 'war',
taskInfo: '猎杀时刻'
}
// 任务大殿通知购买权限弟子
subject.notify(warTask)
8.发布-订阅模式
class PubSub {
constructor() {
// 事件中心
// 存储格式: warTask: [], routeTask: []
// 每种事件(任务)下存放其订阅者的回调函数
this.events = {}
}
// 订阅方法
subscribe(type, cb) {
if (!this.events[type]) {
this.events[type] = [];
}
this.events[type].push(cb);
}
// 发布方法
publish(type, ...args) {
if (this.events[type]) {
this.events[type].forEach(cb => cb(...args))
}
}
// 取消订阅方法
unsubscribe(type, cb) {
if (this.events[type]) {
const cbIndex = this.events[type].findIndex(e=> e === cb)
if (cbIndex != -1) {
this.events[type].splice(cbIndex, 1);
}
}
if (this.events[type].length === 0) {
delete this.events[type];
}
}
unsubscribeAll(type) {
if (this.events[type]) {
delete this.events[type];
}
}
}
// 创建一个中介公司
let pubsub = new PubSub();
// 弟子一订阅战斗任务
pubsub.subscribe('warTask', function (taskInfo){
console.log("宗门殿发布战斗任务,任务信息:" + taskInfo);
})
// 弟子一订阅战斗任务
pubsub.subscribe('routeTask', function (taskInfo) {
console.log("宗门殿发布日常任务,任务信息:" + taskInfo);
});
// 弟子三订阅全类型任务
pubsub.subscribe('allTask', function (taskInfo) {
console.log("宗门殿发布五星任务,任务信息:" + taskInfo);
});
// 发布战斗任务
pubsub.publish('warTask', "猎杀时刻");
pubsub.publish('allTask', "猎杀时刻");
// 发布日常任务
pubsub.publish('routeTask', "种树浇水");
pubsub.publish('allTask', "种树浇水");
总结
上文中提到了观察者模式和发布——订阅模式,我们来总结一下两者差异:
静态资源和动态资源
静态资源:可以理解为前端的固定页面,这里面包含HTML、CSS、JS、图片等等,不需要查数据库也不需要程序处理,直接就能够显示的页面,如果想修改内容则必须修改页面,但是访问效率相当高。 具体形式为:客户端发送请求到web服务器,web服务器拿到对应的文件,返回给客户端,客户端解析并渲染出来。
动态资源:需要程序处理或者从数据库中读数据,能够根据不同的条件在页面显示不同的数据,内容更新不需要修改页面但是访问速度不及静态页面。 具体形式为:客户端请求的动态资源,先把请求交给web的一个存储点,web存储点连接数据库,数据库处理数据之后,将数据交给web服务器,web服务器返回给客户端解析渲染处理。 区别:
- 静态资源一般都是设计好的html页面,而动态资源依靠设计好的程序来实现按照需求的动态响应或者从数据库中读数据;
- 静态资源的交互性差,不好更改,而动态资源可以根据需求获取内容;
- 在服务器的运行状态不同,静态资源不需要与数据库参于程序处理,动态资源需要一个或多个数据库的参与运算。
js获取原型的方法
- p.proto
- p.constructor.prototype
- Object.getPrototypeOf(p)
function Pro(girl) {
this.girl = girl
}
Pro.prototype.message = function () {
console.log('信息')
}
const a = new Pro('女孩')
console.log('aaa', Object.getPrototypeOf(a).message())
console.log('aaa',a.__proto__.message())
console.log('aaa', a.constructor.prototype.message())
什么是类?
类(class)是在 JS 中编写构造函数的新方法。它是使用构造函数的语法糖,在底层中使用仍然是原型和基于原型的继承。
function Person(firstName, lastName, age, address) {
this.firstName = firstName
this.lastName = lastName
this.age = age
this.address = address
}
Person.self = function () {
return this
}
Person.prototype.message = function () {
console.log('----信息')
}
Person.prototype.toString = function () {
return "[object Person]"
}
Person.prototype.getFullName = function () {
return `${this.firstName}-${this.lastName}`
}
const a = new Person('a','b','c','d')
console.log('a', a.toString())
// es6
class Person02 {
constructor(fistName,lastName,age,address) { // 记住props表示的是一个属性,不是对象
this.fistName = fistName
this.lastName = lastName
this.age = age
this.address = address
}
toString() {
return '[Object, string]'
}
getFullName() {
return `${this.fistName}-${this.lastName}`
}
}
const b = new Person02('a','b','c','d')
console.log('b',b.getFullName())
类(class)的继承
// es5继承
Employee.prototype = Object.create(Person.prototype)
function Employee(firstName,lastName,age,address,jobTitle,yearStarted) {
Person.call(this, firstName, lastName, age, address)
this.jobTitle = 'title'
this.yearStarted = yearStarted
}
Employee.prototype.describe = function () {
console.log('describe')
}
Employee.prototype.toString = function () {
console.log('toString')
}
const abcde = new Employee('a','b','c','d','e','f')
console.log('aaaaa',abcde.firstName, abcde.getFullName())
什么是Set对象,它是如何工作的?
Set是新的数据结构,它类似于数组,但是成员的值是唯一的,没有重复。 Set本身就是构造函数
const set = new Set()
[2,3,5,4,5,2,2].forEach(x=>set.add(x))
// 转为数组
const array = [...set]
// Set不会发生类型转换,“5”,5就是不同值,Set类似于===,唯一的区别就是Set的NaN等于自身,但是NaN在精确相等这块是不等于自身
Set实例的属性和方法
Set结构的实例有以下几个属性和方法:
const set = new Set(['1','2','3'])
set.size // 3
set.add(4) // ['1','2','3',4] 添加某个值,返回Set结构本身
set.delete('2') // ['1','3',4] 删除某个值,返回一个布尔值,表示删除是否成功
set.has(3)// false
set.clear() // 清除所有成员,没有返回值
Set遍历操作 1.keys(): 返回键名的遍历器 2.values(): 返回键值的遍历器 3.entries(): 返回键值对的遍历器 4.forEach(): 使用回调函数遍历每个成员
let set = new Set(['red','green', 'blue'])
for(let item of set.keys()) {
console.log(item)
}
// red
// green
// blue
for(let item of set.values) {
console.log(item)
}
// red
// green
// blue
for(let item of set.entries()) {
console.log(item)
}
// [red, red]
// [green, green]
// [blue, blue]
// forEach进行操作
如果要使用数组方法==>将set数据结构转换成数组即可
WeakSet与Set的区别:1.WeakSet的成员只能是对象,而不能是其他类型的值。2.WeakSet没有size方法。3.不能遍历
const a = [3,4]
const ws = new WeakSet(a) // 报错,因为成员,3,4不是对象
const a = [[2],[4]]
const ws = new WeakSet(a) // 正常
const a = [{abc: '12'},{dce: '45'}]
const ws = new WeakSet(a) // 正常
WeakSet方法:
1.add
2.delete
3.has
什么是Map对象,它是如何工作的?
ES6提供了Map数据结构,它类似于对象,也是键值对的集合,但是键的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object结构提供了“字符串--值”的对应,Map结构提供了“值-值”的对应,是一种更完善的Hash结构实现。如果需要“键值对”的数据结构,Map比Object更合适
const m = new Map()
const o = {p: 'Hello world'}
m.set(o, 'content')
m.get(o) // 'content'
m.has(o) // true
m.delete(o)
m.clear()
Map也可以通过接受一个数组作为参数
const map = new Map([['name': 'zhang'],['title','island']])
map.size //2
map.has('name') //true
map.get('name') // 'zhang'
map.has('title') // true
map.get('title') // 'island'
如果Map的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map就将其视为一个键,包括 0和-0。另外NAN不严格等于自己,但是Map视作同一个键
const map = new Map()
map.set(-0, 123)
map.set(+0) // 123
Map遍历方法
1.keys()
2.values()
3.entries()
4.forEach()
Map和WeakMap的区别: WeakMap的键名只能是对象,不能是其它的
什么是Proxy?
Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”,即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
const obj = new Proxy({},{
get: function (target,key,receiver) {
console.log(`getting ${key}`)
return Reflect.get(target,key,receiver)
},
set: function (target,key,value,receiver) {
console.log(`setting ${key}`)
return Reflect.set(target,key,receiver)
}
})
上面的代码中,构造函数Proxy接受两个参数:第一个参数是所要代理的目标对象(上例中是一个空对象),即如果没有Proxy介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。 Proxy实例方法: 1.get()
const person = {
name: '张三'
}
const proxy = new Proxy(person, {
get: function (target,property) {
if(property in person) {
return target[property]
}
}
})
- set()
- apply()
- has()
has方法用来拦截HasProperty操作 - construct()
拦截new命令 - deleteProperty()
拦截delete命令 - defineProperty()
拦截Object.defineProperty() - getOwnPropertyDescriptor()
拦截Object.getOwnPropertyDescriptor() - getPrototypeOf()
拦截获取对象原型 - isExtensible() 拦截Obeject.isExtensible操作
- ownKeys() 拦截对象自身属性的读取操作
- preventExtensions() 拦截Object.preventExtensions() 13.setPrototypeOf() 拦截Object.setPrototypeOf方法
写一个通用的事件侦听器函数
const EventUtils = {
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 添加事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
// 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},
// 获取事件目标
getTarget: function(event) {
return event.target || event.srcElement;
},
// 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
getEvent: function(event) {
return event || window.event;
},
// 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
// 取消事件的默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
什么是函数式编程? JavaScript的哪些特性使其成为函数式语言的候选语言?
函数式编程(通常缩写为FP)是通过编写纯函数,避免共享状态、可变数据、副作用 来构建软件的过程。数式编程是声明式 的而不是命令式 的,应用程序的状态是通过纯函数流动的。与面向对象编程形成对比,面向对象中应用程序的状态通常与对象中的方法共享和共处。
函数式编程是一种编程范式 ,这意味着它是一种基于一些基本的定义原则(如上所列)思考软件构建的方式。当然,编程范式的其他示例也包括面向对象编程和过程编程。
函数式的代码往往比命令式或面向对象的代码更简洁,更可预测,更容易测试 - 但如果不熟悉它以及与之相关的常见模式,函数式的代码也可能看起来更密集杂乱,并且 相关文献对新人来说是不好理解的。
什么是高阶函数?
高阶函数只是将函数作为参数或返回值的函数。
function higherOrderFunction(param,callback){
return callback(param);
}
手写实现Array.prototype.map方法
function filter(arr, filterCallback) {
if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function')
{
return [];
} else {
let result = [];
for (let i = 0, len = arr.length; i < len; i++) {
if (filterCallback(arr[i], i, arr)) {
result.push(arr[i]);
}
}
return result;
}
}
手写实现Array.prototype.filter方法
function filter(arr, filterCallback) {
// 首先,检查传递的参数是否正确。
if (!Array.isArray(arr) || !arr.length || typeof filterCallback !== 'function')
{
return [];
} else {
let result = [];
// 每次调用此函数时,我们都会创建一个 result 数组
// 因为我们不想改变原始数组。
for (let i = 0, len = arr.length; i < len; i++) {
// 检查 filterCallback 的返回值是否是真值
if (filterCallback(arr[i], i, arr)) {
// 如果条件为真,则将数组元素 push 到 result 中
result.push(arr[i]);
}
}
return result; // return the result array
}
}
手动实现Array.prototype.reduce方法
function reduce(arr, reduceCallback, initialValue) {
// 首先,检查传递的参数是否正确。
if (!Array.isArray(arr) || !arr.length || typeof reduceCallback !== 'function')
{
return [];
} else {
// 如果没有将initialValue传递给该函数,我们将使用第一个数组项作为initialValue
let hasInitialValue = initialValue !== undefined;
let value = hasInitialValue ? initialValue : arr[0];
、
// 如果有传递 initialValue,则索引从 1 开始,否则从 0 开始
for (let i = hasInitialValue ? 1 : 0, len = arr.length; i < len; i++) {
value = reduceCallback(value, arr[i], i, arr);
}
return value;
}
}
遍历器Iterator的概念
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据机构提供统一的访问机制。任何数据结构,只要部署Iterator接口,就可以完成遍历操作。
Iterator的作用有3个:
1.为各种数据结构提供一个统一的、简单的访问接口;
2.使得数据结构的成员能够按某种次序排列;
3.ES6创造了一种新的遍历命令---for....of循环,iterator接口主要供for....of消费
function makeIterator(array) {
let nextIndex = 0
return {
next: function () {
return nextIndex < array.length ? {value: array[nextIndex++]} : {done: true}
}
}
}
let it = makeIterator(['a','b'])
console.log(it.next())
console.log(it.next())
默认Iterator接口
ES6规定,默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是可遍历的。调用Symbol.iterator方法,我们就会得到当前数据结构默认的遍历器生成函数,Symbol.iterator本身是一个表达式,返回Symbol对象的iterator属性,这是一个与定义好的、类型为Symbol的特殊值,所以要放在方括号中。
ES6的有些数据结构原生具备Iterator接口(比如数组),即不用任何处理就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性的数据结构都称为部署了遍历器的接口。
原生具备iterator接口如下:
1.Array。
2.Map。
3.Set。
4.String。
for....of循环
类似数组的对象
// 字符串
let str = "hello"
for(let s of str) {
console.log(s) // h e l l o
}
// DOM NodeList对象
let paras = document.querySelectorAll('p')
for(let p of paras) {
p.classList.add('test')
}
// arguments对象
function printArgs() {
for(let x of arguments) {
console.log(x)
}
}
对象
对于普通的对象,for...of结构不能直接使用,否则会报错,必须部署了iterator接口才能使用。但是,对这种情况下,for...in循环依然可用于遍历键名。
与其他遍历语法的比较
以数组为例,for循环写法比较麻烦
forEach无法中途跳出,break命令,return命令
for....in循环有几个缺点:
1.for....in循环不仅可以遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
2.某些情况下,for...in循环会以任意顺序遍历键名
for....of:
1.没有for...in缺点。
2.不同于forEach方法,它可以与break,continue和return配合使用。
3.提供了遍历所有数据结构的统一操作接口。
Generator函数是什么,有什么作用?
Generator函数可以说是Iterator接口的具体实现方式。Generator 最大的特点就是可以控制函数的执行。
function *foo(x) {
let y = 2 * (yield (x + 1))
let z = yield (y / 3)
return (x + y + z)
}
let it = foo(5)
console.log(it.next()) // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}
js的深浅拷贝
浅拷贝:创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
浅拷贝的实现方式:
Object.assign()方法:
let a = {
name: "jake",
flag: {
title: "better day by day",
time: '2020-05-31'
}
}
const c = Object.assign(a)
c.name = '123'
console.log(c) // '123'
console.log(a) // '123'
函数库lodash的clone方法
var _ = require('lodash');
var obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true
如果是数组,可以使用如下方法:
Array.prototype.concat()
let arr = [1, 3, {
username: 'kobe'
}];
let arr2 = arr.concat();
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
console.log(arr === arr2) // false
Array.prototype.slice() //slice方法返回一个新的数组对象,这一对象是一个由begin和end(不包括end)决定的原数组的浅拷贝。原始数组不会被改变
let arr = [1, 3, {
username: ' kobe'
}];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]
console.log(arr === arr3) // false
扩展运算符(...)
let a = {
name: "Jake",
flag: {
title: "better day by day",
time: "2020-05-31"
}
}
let b = {...a};
深拷贝的实现方式:
1.乞丐版:
JSON.parse(JSON.stringify(object)),缺点诸多(会忽略undefined、symbol、函数;不能解决循环引用;不能处理正则、new Date())
var obj1={
name:'大雄',
say:function(){
console.log('我会说话哦!');
sex: undefined,
age: NaN,
},
birthday:new Date('1990/01/01')
};
var obj2=JSON.parse(JSON.stringify(obj));
console.log(obj2);
// {name: "大雄", age: null, birthday: "1989-12-31T16:00:00.000Z"}
我们看到了,当我们的对象中有函数和日期类型是,日期类型被转化成了字符串;函数属性直接没有了!是不是问题很大。
- undefined、任意的函数以及 symbol 值,在序列化过程中会被忽略
- Date 日期调用了 toJSON() 将其转换为了 string 字符串(Date.toISOString()),因此会被当做字符串处理。
- NaN 和 Infinity 格式的数值及 null 都会被当做 null。
- 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。
- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
// 下面就是循环引用;
var obj1 = {
x: 1,
y: 2
};
obj1.z = obj1;
var obj2 = JSON.parse(JSON.stringify(obj1)); // 栈溢出,抛出错误;
2.基础篇:
function cloneDeep(target) {
if(typeOf taret ==='object'){
let cloneTarget = Array.isArray(target) ? [] : {};
for(const key in target){
cloneTarget[key] = cloneDeep(target[key]);
}
return cloneTarget
}else {
return target
}
}
babel
1.@babel/preset-env :是一个智能预设,会根据当前的环境进行语法转换。自动的确定babel插件和polyfills.没有任何配置的情况下,和babel-preset-latest一样。所谓的babel-polyfill可以将新的api进行转换,如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象。
targets: 描述您为项目配置的支持/定位的环境。
modules: 启用将es6转换为其他模块
debug:是否启用console.log
{
"presets": [
["env", {
"modules": false,
"targets": {
"browsers": ["> 1%", "last 2 versions", "not ie 小于等于8"]
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime", "ramda"],
}
2.@babel/preset-react
这都是一些react相关的集合
{
"presets": ["react"]
}
3.transform-runtime插件
对 babel 转化后的代码,进行再次转化,与 babel-polyfill 一样,解决不兼容的全局 api 。
与 babel-polyfill 不同是它不是添加/修改全局对象。它是对不兼容的方法进行特殊处理,也就是添加辅助方法来做兼容。并且 transform-runtime 是在需要进行兼容转化时候引用。transform-runtime 是依赖 babel-runtime ,且辅助方法都是引用的 babel-runtime
helpers : 是否使用 @babel-runtime/helpers 来代替内部的 helpers
coresjs : 是否用 @babel-runtime/corejs 中的辅助方法来替换 Map / Set 等方法
polyfill : 是否用 @babel-runtime 的辅助函数来代替 polyfill
regenerator : 是否用 辅助函数来代替 async / generator 函数
moduleName : 引用时候名字
{
"presets": [
"react",
["env",{...}]
"stage-0"
],
"plugins": ["path","file"],
"env": { // 不同的环境配置一套.babelrc
"test": {},
"pre": {},
"plugins": {}
}
}
4.babel-core
如果某些代码需要调用babel的API进行转码,就要使用babel-core模块
Reflect
对操作对象提供了一个新的API。
Reflect对象一共有13个静态方法
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
1.Reflect.get(target, name, receiver)
const a = {
name: 'zhang',
age: 25,
baz: function () {
return this.name + this.age
},
get bax() {
return this.name + this.age
}
}
console.log('Reflect', Reflect.get(a,'baz')) // function
console.log('Reflect', Reflect.get(a,'bax')) // zhang25
console.log('Reflect', Reflect.get(a,'bat')) // undefined
如果第一个参数不是对象,get方法就会报错
Reflect.get(1,'foo') //报错
Reflect.get(false, 'foo') //报错
2.Reflect.set(target, name, value, receiver)
Reflect.set方法设置target对象的name属性等于value
var myObject = {
foo: 1,
set bar(value) {
return this.foo = value;
},
}
myObject.foo // 1
Reflect.set(myObject, 'foo', 2);
myObject.foo // 2
Reflect.set(myObject, 'bar', 3)
myObject.foo // 3
如果name属性设置了赋值函数,则赋值函数的this绑定receiver。但是如果你没有设置receiver,则this绑定的还是myObject,即默认receiver表示myObject
var myObject = {
foo: 4,
set bar(value) {
return this.foo = value;
},
};
var myReceiverObject = {
foo: 0,
};
Reflect.set(myObject, 'bar', 1, myReceiverObject);
myObject.foo // 4
myReceiverObject.foo // 1
注意,如果 Proxy对象和 Reflect对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。
let p = {
a: 'a'
};
let handler = {
set(target, key, value, receiver) {
console.log('set');
Reflect.set(target, key, value, receiver)
},
defineProperty(target, key, attribute) {
console.log('defineProperty');
Reflect.defineProperty(target, key, attribute);
}
};
let obj = new Proxy(p, handler);
obj.a = 'A';
// set
// defineProperty
上面代码中,Proxy.set拦截里面使用了Reflect.set,而且传入了receiver,导致触发Proxy.defineProperty拦截。这是因为Proxy.set的receiver参数总是指向当前的 Proxy实例(即上例的obj),而Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。
如果第一个参数不是对象,Reflect.set会报错。
Reflect.set(1, 'foo', {}) // 报错
Reflect.set(false, 'foo', {}) // 报错
3.Reflect.has(obj, name)
Reflect.has方法对应name in obj里面的in运算符。
var myObject = {
foo: 1,
};
// 旧写法
'foo' in myObject // true
// 新写法
Reflect.has(myObject, 'foo') // true
如果Reflect.has()方法的第一个参数不是对象,会报错。
4.Reflect.deleteProperty(obj, name)
Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性。
const myObj = { foo: 'bar' };
// 旧写法
delete myObj.foo;
// 新写法
Reflect.deleteProperty(myObj, 'foo');
该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。
如果Reflect.deleteProperty()方法的第一个参数不是对象,会报错。
5. Reflect.construct(target,args)
Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。
function Greeting(name) {
this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);
6.Reflect.getPrototypeOf(obj)
Reflect.getPrototypeOf方法用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)。
const myObj = new FancyThing();
// 旧写法
Object.getPrototypeOf(myObj) === FancyThing.prototype;
// 新写法
Reflect.getPrototypeOf(myObj) === FancyThing.prototype;
Reflect.getPrototypeOf和Object.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。
Object.getPrototypeOf(1) // Number {[[PrimitiveValue]]: 0}
Reflect.getPrototypeOf(1) // 报错
7.Reflect.setPrototypeOf(obj, newProto)
Reflect.setPrototypeOf方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。
const myObj = {};
// 旧写法
Object.setPrototypeOf(myObj, Array.prototype);
// 新写法
Reflect.setPrototypeOf(myObj, Array.prototype);
myObj.length // 0
如果无法设置目标对象的原型(比如,目标对象禁止扩展),Reflect.setPrototypeOf方法返回false。
Reflect.setPrototypeOf({}, null)
// true
Reflect.setPrototypeOf(Object.freeze({}), null)
// false
如果第一个参数不是对象,Object.setPrototypeOf会返回第一个参数本身,而Reflect.setPrototypeOf会报错。
Object.setPrototypeOf(1, {})
// 1
Reflect.setPrototypeOf(1, {})
// TypeError: Reflect.setPrototypeOf called on non-object
如果第一个参数是undefined或null,Object.setPrototypeOf和Reflect.setPrototypeOf都会报错。
Object.setPrototypeOf(null, {})
// TypeError: Object.setPrototypeOf called on null or undefined
Reflect.setPrototypeOf(null, {})
// TypeError: Reflect.setPrototypeOf called on non-object
- Reflect.apply(func, thisArg, args)
Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。
一般来说,如果要绑定一个函数的this对象,可以这样写fn.apply(obj, args),但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作。
const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);
const fancy = {
a: 123,
b: 789
}
function Things() {
this.b = 125
Things.prototype.getMessage = function () {
this.b = this.b + 1
console.log('this.b', this.b)
}
}
Reflect.apply(new Things().getMessage,fancy,[])
9.Reflect.defineProperty(target, propertyKey, attributes)
attributes: 要定义或修改的属性的描述。
Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。
const myDate = {}
Reflect.defineProperty(myDate,'now', {
value: 123
})
console.log('MyDate', myDate)
10.Reflect.ownKeys (target)
Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和。
var myObject = {
foo: 1,
bar: 2,
[Symbol.for('baz')]: 3,
[Symbol.for('bing')]: 4,
};
// 旧写法
Object.getOwnPropertyNames(myObject)
// ['foo', 'bar']
Object.getOwnPropertySymbols(myObject)
//[Symbol(baz), Symbol(bing)]
// 新写法
Reflect.ownKeys(myObject)
// ['foo', 'bar', Symbol(baz), Symbol(bing)]
ONCE函数
function once(func) {
var tag = true;
return function () {
if(tag) {
tag = false
return func.call(null, ...arguments)
}
return undefined
}
}
const addOnce = once(function(a, b) {
console.log('ab',a,b)
return a + b;
})
const abc = addOnce(1,2)
实现call,bind,apply函数
call 实现
Function.prototype.myCall = function (target, ...args) {
if(typeof this !== "function") {
throw new TypeError("not a function")
}
target = target || window
target.fn = this
let result = target.fn(...args)
return result
}
let obj = { name: 123 }
function foo(...args) {
console.log(this.name, args)
}
let s = foo.myCall(obj, "111", "222")
bind 实现
Function.prototype.myBind = function (target, ...args) {
const fn = this
args = args ? args: []
return function newFn(...newFnArgs) {
return fn.apply(target, [...args, ...newFnArgs])
}
}
apply实现
Function.prototype.myApply = function (target, args) {
if(typeof this !== "function") {
throw new Error("not a function")
}
if(!Array.isArray(args)) {
throw new Error("args not a array")
}
target = target || window
target.fn = this
let result = target.fn(...args)
return result
}
let obj = { name: 123 }
function foo(...args) {
console.log(this.name, args)
}
let s = foo.myApply(obj, ["111", "222"]
reduce实现
Array.prototype.myReduce = function (callback, initValue) {
var arr = this
if(!Array.isArray(arr)) {
throw new Error('not a array')
}
if(!arr.length) {
throw new Error('selfReduce of empty array with no initial value')
}
let acc = null
if(initValue === undefined) {
acc = arr[0]
} else {
acc = initValue
}
for (let i=0; i<arr.length; i++) {
if(initValue === undefined && (i+1) === originArray.length) break;
acc = callback(acc, initValue === undefined ? arr[i+1]: arr[i], i, arr)
}
return acc
}
柯里化函数
var curring = () => {
var result = [];
var add = (...args) => {
result = result.concat(args);
return add;
};
add.valueOf = add.toString = () => {
console.log('1212', result.reduce((pre, cur) => pre + cur, 0))
return result.reduce((pre, cur) => pre + cur, 0);
}
return add;
};
var add = curring();
console.log(add);
add = curring();
console.log(add(1, 2)(3, 4)(5, 6));
add = curring();
console.log(add(1, 2, 3, 4, 5, 6));
树形结构转成列表
[
{
id: 1,
text: '节点1',
parentId: 0,
children: [
{
id:2,
text: '节点1_1',
parentId:1
}
]
}
]
转成
[
{
id: 1,
text: '节点1',
parentId: 0 //这里用0表示为顶级节点
},
{
id: 2,
text: '节点1_1',
parentId: 1 //通过这个字段来确定子父级
}
...
]
function treeToList(data) {
let res = [];
const dfs = (tree) => {
tree.forEach((item) => {
if (item.children) {
dfs(item.children);
delete item.children;
}
res.push(item);
});
};
dfs(data);
return res;
}
大数相加
let a = "9007199254740991";
let b = "1234567899999999999";
function add(a, b) {
let maxLength = Math.max(a.length, b.length)
let newA = a
let newB = b
if(maxLength > a.length) {
const fillCount = maxLength - a.length
const newFillArr = new Array(maxLength - a.length)
newFillArr.fill(0)
newA = newFillArr.concat(a.split('')).join('')
}
if(maxLength > b.length) {
const fillCount = maxLength - b.length
const newFillArr = new Array(maxLength - b.length)
newFillArr.fill(0)
newB = newFillArr.concat(b.split('')).join('')
}
//定义加法过程中需要用到的变量
let t = 0;
let f = 0; //"进位"
let sum = "";
for(let i=maxLength-1 ; i>=0 ; i--){
t = parseInt(newA[i]) + parseInt(newB[i]) + f;
f = Math.floor(t/10);
sum = t%10 + sum;
}
if(f!==0){
sum = '' + f + sum;
}
return sum;
}
列表转成树形结构
function listToTree(data) {
let temp = {};
let treeData = [];
for (let i = 0; i < data.length; i++) {
temp[data[i].id] = data[i];
}
for (let i in temp) {
if (+temp[i].parentId != 0) {
if (!temp[temp[i].parentId].children) {
temp[temp[i].parentId].children = [];
}
temp[temp[i].parentId].children.push(temp[i]);
} else {
treeData.push(temp[i]);
}
}
return treeData;
}
实现一个对象的 flatten 方法
const obj = {
a: {
b: 1,
c: 2,
d: {e: 5}
},
b: [1, 3, {a: 2, b: 3}],
c: 3
}
flatten(obj) 结果返回如下
// {
// 'a.b': 1,
// 'a.c': 2,
// 'a.d.e': 5,
// 'b[0]': 1,
// 'b[1]': 3,
// 'b[2].a': 2,
// 'b[2].b': 3
// c: 3
// }
function isObject(val) {
return typeof val === "object" && val !== null;
}
function flatten(obj) {
if (!isObject(obj)) {
return;
}
let res = {};
const dfs = (cur, prefix) => {
if (isObject(cur)) {
if (Array.isArray(cur)) {
cur.forEach((item, index) => {
dfs(item, `${prefix}[${index}]`);
});
} else {
for (let k in cur) {
dfs(cur[k], `${prefix}${prefix ? "." : ""}${k}`);
}
}
} else {
res[prefix] = cur;
}
};
dfs(obj, "");
return res;
}
flatten();
实现模板字符串解析功能
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
name: '姓名',
age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(template, data) {
let computed = template.replace(/\{\{(\w+)\}\}/g, function (match, key) {
return data[key];
});
return computed;
}
类数组转化为数组的方法
const arrayLike=document.querySelectorAll('div')
// 1.扩展运算符
[...arrayLike]
// 2.Array.from
Array.from(arrayLike)
// 3.Array.prototype.slice
Array.prototype.slice.call(arrayLike)
// 4.Array.apply
Array.apply(null, arrayLike)
// 5.Array.prototype.concat
Array.prototype.concat.apply([], arrayLike)
实现一个基本的Promise
// 未添加异步处理等其他边界情况
// ①自动执行函数,②三个状态,③then
class Promise {
constructor (fn) {
// 三个状态
this.state = 'pending'
this.value = undefined
this.reason = undefined
let resolve = value => {
if (this.state === 'pending') {
this.state = 'fulfilled'
this.value = value
}
}
let reject = value => {
if (this.state === 'pending') {
this.state = 'rejected'
this.reason = value
}
}
// 自动执行函数
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
}
// then
then(onFulfilled, onRejected) {
switch (this.state) {
case 'fulfilled':
onFulfilled()
break
case 'rejected':
onRejected()
break
default:
}
}
}
实现一个基本的Event Bus
// 组件通信,一个触发与监听的过程
class EventEmitter {
constructor () {
// 存储事件
this.events = this.events || new Map()
}
// 监听事件
addListener (type, fn) {
if (!this.events.get(type)) {
this.events.set(type, fn)
}
}
// 触发事件
emit (type) {
let handle = this.events.get(type)
handle.apply(this, [...arguments].slice(1))
}
}
// 测试
let emitter = new EventEmitter()
// 监听事件
emitter.addListener('ages', age => {
console.log(age)
})
// 触发事件
emitter.emit('ages', 18) // 18
实现once函数
function ones(func) {
var flag = true
return function() {
if(flag) {
func.call(null, ...arguments)
flag = false
} else {
return undefined
}
}
用setTimeout实现setInterval
const mySetInterval = (cb, time) => {
const fn = () => {
cb()
setTimeout(()=> {
fn()
},time)
}
setTimeout(fn, time)
}
用clearTimeout实现clearInterval
let timeMap = {}
let id = 0 // 简单实现id唯一
const mySetInterval = (cb, time) => {
let timeId = id // 将timeId赋予id
id++ // id 自增实现唯一id
let fn = () => {
cb()
timeMap[timeId] = setTimeout(() => {
fn()
}, time)
}
timeMap[timeId] = setTimeout(fn, time)
return timeId // 返回timeId
}
const myClearInterval = (id) => {
clearTimeout(timeMap[id]) // 通过timeMap[id]获取真正的id delete timeMap[id]
}
实现sleep函数
function sleep(ms) {
const template = new Promise(
(resolve => {
setTimeout(resolve, ms)
})
)
return template
}
js类型判断
1.typeof
typeof返回一个表示数据类型的字符串,返回结果包括:number、string、boolean、object、undefined、function。typeof可以对基本类型number、string 、boolean、undefined做出准确的判断(null除外,typeof null===“object”,这是由于历史的原因,我就不巴拉巴拉了,其实我也说不清楚😢);而对于引用类型,除了function之外返回的都是object。但当我们需要知道某个对象的具体类型时,typeof就显得有些力不从心了。
typeof 1 // number
typeof '' // string
typeof true //boolean
typeof undefined //undefined
typeof null // object
typeof function () // function
typeof [] //object
typeof new Date() //object
typeof new RegExp() //object
2.instanceof
当我们需要知道某个对象的具体类型时,可以用运算符 instanceof,instanceof操作符判断左操作数对象的原型链上是否有右边这个构造函数的prototype属性,也就是说指定对象是否是某个构造函数的实例,最后返回布尔值。instanceof运算符只能用于对象,不适用原始类型的值。
[] instanceof Array // true
[] instanceof Object // true
new Date() instanceof Date // true
new Date() instanceof Object // true
function Person() {}
new Person() instanceof Person // true
new Person() instanceof Object // true
3.Object.prototype.toString
toString是Object原型对象上的一个方法,该方法默认返回其调用者的具体类型,更严格的讲,是 toString运行时this指向的对象类型, 返回的类型格式为[object,xxx],xxx是具体的数据类型,其中包括:String,Number,Boolean,Undefined,Null,Function,Date,Array,RegExp,Error,HTMLDocument,... 基本上所有对象的类型都可以通过这个方法获取到。
Object.prototype.toString.call('') ; // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object Window]
var、let、const之间的区别
var声明变量可以重复声明,而let不可以重复声明
var是不受限于块级的,而let是受限于块级
var会与window相映射(会挂一个属性),而let不与window相映射
var可以在声明的上面访问变量,而let有暂存死区,在声明的上面访问变量会报错
const声明之后必须赋值,否则会报错
const定义不可变的量,改变了就会报错
const和let一样不会与window相映射、支持块级作用域、在声明的上面访问变量会报错
使用箭头函数应注意什么
(1)用了箭头函数,this就不是指向window,而是父级(指向是可变的)
(2)不能够使用arguments对象
(3)不能用作构造函数,这就是说不能够使用new命令,否则会抛出一个错误
(4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数
##ES6的模板字符串有哪些新特性?并实现一个类模板字符串的功能
let name = 'web';
let age = 10;
let str = '你好,${name} 已经 ${age}岁了'
str = str.replace(/\$\{([^}]*)\}/g,function(){
return eval(arguments[1]);
})
console.log(str);//你好,web 已经 10岁了
Promise构造函数是同步执行还是异步执行,那么 then 方法呢?
promise构造函数是同步执行的,then方法是异步执行的
理解 async/await以及对Generator的优势
async await 是用来解决异步的,async函数是Generator函数的语法糖
使用关键字async来表示,在函数内部使用 await 来表示异步
async函数返回一个 Promise 对象,可以使用then方法添加回调函数
当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句
async较Generator的优势:
(1)内置执行器。Generator 函数的执行必须依靠执行器,而 Aysnc 函数自带执行器,调用方式跟普通函数的调用一样
(2)更好的语义。async 和 await 相较于 * 和 yield 更加语义化
(3)更广的适用性。yield命令后面只能是 Thunk 函数或 Promise对象,async函数的await后面可以是Promise也可以是原始类型的值
(4)返回值是 Promise。async 函数返回的是 Promise 对象,比Generator函数返回的Iterator对象方便,可以直接使用 then() 方法进行调用
forEach、for in、for of三者区别
forEach更多的用来遍历数组
for in 一般常用来遍历对象或json
for of 数组对象都可以遍历,遍历对象需要通过和Object.keys()
for in 循环出的是key,for of循环出的是value
解构赋值
// 数组解构赋值
let [x, y, z] = new Set(['a', 'b', 'c']) // x=> "a"
//
字符串的扩展
// 字符串的遍历
for(let codePoint of 'foo') {
console.log(codePoint) // "f","o","o"
}
// includes,startsWith, endsWidth
const s = "hello world!"
s.startsWith('hello') // true
s.endsWith('!') // true
s.includes('o') // true
s.startsWidth('world', 6) // true
s.endsWidth('hello', 5) // true
endsWidth不同点:针对前n个字符
'x'.repeat(2) //'xx'
'x'.repeat(1) // 'x'
'na'.repeat(0) // ''
'na'.repeat(2.9) // 'nana' 取整
'padStart': 头部补齐
'padEnd': 尾部补齐
'x'.padStart(5, 'ab')//ababx,5表示最小长度
'x'.padEnd(4, 'ab') //xaba,4表示最小长度
'xxx'.padStart(2, 'ab') // 'xxx'
'xxx'.padEnd(2, 'ab') // 'xxx'
'abc'.padStart(10, '0123456789') // '0123456abc'
'x'.padStart(4) // ' x'
'x'.padEnd(4) // 'x '
padStart的常见用途是为数值补全指定位数。