变量
值类型:
string
,number
,boolean
,undefined
,null
,symbol
引用类型:
object
,array
,function
typeof
操作符: 识别值类型,能判断function
,能识别引用类型,但不能识别具体(细分)的引用类型。- 识别值类型:
string
,number
,boolean
,undefined
,symbol
- 识别函数:
function
- 识别引用类型:
object
- 识别值类型:
instanceof
操作符:识别引用类型,能识别具体(细分)的引用类型,不能识别值类型。
// js 类型判断
function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1).toLowerCase()
}
// 深拷贝 JSON.parse(JSON.stringify())
// 不足:无法拷贝函数,无法拷贝 undefined,无法拷贝循环引用
// JSON.stringify() 方法无法处理某些特殊的 JavaScript 对象,如 RegExp、Error、Date 等,这些对象会被转换成空对象。
// 对象的原型链上的属性会丢失
// 深拷贝
function deepClone(value) {
const type = getType(value)
let result
if (type === 'object') {
result = {}
} else if (type === 'array') {
result = []
} else {
// 值类型直接返回
return value
}
for (let key in value) {
if (value.hasOwnProperty(key)) {
result[key] = deepClone(value[key])
}
}
return result
}
原型与原型链
__proto__
:隐式原型,每个对象都有__proto__
属性,指向创建它的构造函数的原型对象。prototype
:显式原型,每个函数都有prototype
属性,指向一个对象,这个对象包含可以由特定类型的所有实例共享的属性和方法。constructor
:构造函数,每个原型都有constructor
属性,指向关联的构造函数。原型链:当访问一个对象的属性时,会先在这个对象本身上查找,如果没有找到,就会去它的
__proto__
隐式原型上查找,如果还没有找到,就会去__proto__
的__proto__
上查找,一直找到Object
的__proto__
为止。Object.prototype
:所有对象的原型,Object.prototype.__proto__
为null
。Function.prototype
:所有函数的原型,Function.prototype.__proto__
为Object.prototype
。hasOwnProperty
:判断一个对象是否有自己的属性,而不是继承自原型链。class
本质:class
只是一个语法糖,本质还是原型链(函数)。new
操作符:创建一个新对象,将构造函数的作用域赋给新对象(因此this
就指向了这个新对象),执行构造函数中的代码,返回新对象。instanceof
操作符:判断一个对象是否是另一个对象的实例,判断原理是判断对象的原型链上是否有这个构造函数的prototype
。
// 简单手写 jQuery
class jQuery {
// 构造函数
constructor(selector) {
const result = document.querySelectorAll(selector)
const length = result.length
for (let i = 0; i < length; i++) {
this[i] = result[i]
}
this.length = length
this.selector = selector
}
// 获取 DOM 元素
get(index) {
return this[index]
}
// 遍历 DOM 元素
each(fn) {
for (let i = 0; i < this.length; i++) {
const elem = this[i]
fn(elem)
}
}
// 添加事件
on(type, fn) {
return this.each((elem) => {
elem.addEventListener(type, fn, false)
})
}
// 扩展很多 DOM API
}
// 插件
jQuery.prototype.dialog = function (info) {
alert(info)
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {}
style(data) {}
}
// const $p = new jQuery('p')
// $p.get(1)
// $p.each((elem) => console.log(elem.nodeName))
// $p.on('click', () => alert('clicked'))
作用域与闭包
作用域:变量和函数的可访问范围,分为全局作用域和局部作用域。
作用域链:当访问一个变量时,会先在当前作用域查找,如果没有找到,就会去父级作用域查找,一直找到全局作用域为止。
闭包:函数执行时,会创建一个执行上下文,当函数执行完毕,执行上下文会被销毁,但是闭包可以使函数执行完毕后,执行上下文不被销毁,从而使函数内部的变量一直被访问。简单来说,闭包就是能够读取其他函数内部变量的函数。
闭包的作用:1. 读取函数内部的变量;2. 让这些变量的值始终保持在内存中。
闭包的缺点:1. 会造成内存泄漏;2. 会造成性能问题。
this
指向问题:作为普通函数调用,指向全局对象
window
。作为对象方法调用,指向调用对象。
作为构造函数调用,指向实例对象。
作为
class
的方法调用,指向实例对象。使用
call
,apply
,bind
调用,指向绑定的对象。箭头函数,指向定义时所在的对象。
// 闭包隐藏数据,只提供 API
function createCache() {
const data = {} // 闭包中的数据,被隐藏,不被外界访问
return {
set: function (key, val) {
data[key] = val
},
get: function (key) {
return data[key]
},
}
}
const c = createCache()
c.set('a', 100)
console.log(c.get('a'))
// 函数作为返回值
// function create() {
// const a = 100
// return function () {
// console.log(a)
// }
// }
// const fn = create()
// const a = 200
// fn() // 100
// 函数作为参数被传递
function print(fn) {
const a = 200
fn()
}
const a = 100
function fn() {
console.log(a)
}
print(fn) // 100
// 所有的自由变量的查找,是在函数定义的地方,向上级作用域查找
// 不是在执行的地方!!!
// 模拟 bind
Function.prototype.bind1 = function () {
// 将参数拆解为数组
const args = Array.prototype.slice.call(arguments)
// 获取 this(数组第一项)
const t = args.shift()
// fn1.bind(...) 中的 fn1
const self = this
// 返回一个函数
return function () {
return self.apply(t, args)
}
}
function fn1(a, b, c) {
console.log('this', this)
console.log(a, b, c)
return 'this is fn1'
}
const fn2 = fn1.bind1({ x: 100 }, 10, 20, 30)
const res = fn2()
console.log(res) // this is fn1
异步
异步:不等待结果,直接执行后面的代码。
同步:等待结果,再执行后面的代码。
JS 中的异步:
宏任务:
setTimeout
,setInterval
,I/O
,UI rendering
。微任务:
Promise
,process.nextTick
,Object.observe
,MutationObserver
。
事件循环:
JS 是单线程的,同一时间只能做一件事。
事件循环:不断从任务队列中取出任务,放到执行栈中执行。
宏任务队列:每次执行栈执行完毕,就会从宏任务队列中取出一个任务执行。
微任务队列:每次执行栈执行完毕,就会从微任务队列中取出所有任务执行。
执行栈:同步任务会放到执行栈中执行。
宏任务队列和微任务队列是在执行栈执行完毕后,才会执行。
微任务队列的优先级高于宏任务队列。
setTimeout
会将回调函数放到宏任务队列中。Promise
会将回调函数放到微任务队列中。process.nextTick
会将回调函数放到微任务队列中。Object.observe
会将回调函数放到微任务队列中。MutationObserver
会将回调函数放到微任务队列中。setImmediate
会将回调函数放到宏任务队列中。I/O
会将回调函数放到宏任务队列中。UI rendering
会将回调函数放到宏任务队列中。setInterval
会将回调函数放到宏任务队列中。requestAnimationFrame
会将回调函数放到宏任务队列中。process.nextTick
会将回调函数放到微任务队列中。Object.observe
会将回调函数放到微任务队列中。MutationObserver
会将回调函数放到微任务队列中。Promise
会将回调函数放到微任务队列中。setTimeout
会将回调函数放到宏任务队列中。setImmediate
会将回调函数放到宏任务队列中。
Promise
的状态:pending
:进行中。fulfilled
:已成功。rejected
:已失败。
Promise
的特点: 1. 对象的状态不受外界影响;2. 一旦状态改变,就不会再变。Promise
的缺点:1. 无法取消Promise
,一旦新建它就会立即执行,无法中途取消;2. 如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部;3. 当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。async/await
是Promise
的语法糖。for...of
: 用于遍历可迭代对象的元素。
// 手写用promise加载图片
function loadImageAsync(url) {
return new Promise(function (resolve, reject) {
// 创建一个image对象
const image = new Image()
// 绑定事件
image.onload = function () {
resolve(image)
}
// 绑定错误事件
image.onerror = function () {
reject(new Error('Could not load image at' + url))
}
// 设置图片地址
image.src = url
})
}
// 调用
loadImageAsync('https://www.baidu.com/img/bd_logo1.png')
.then(function (image) {
console.log(image)
})
.catch(function (err) {
console.log(err)
})
// setTimeout 试题
console.log(1)
setTimeout(function () {
console.log(2)
}, 1000)
console.log(3)
setTimeout(function () {
console.log(4)
}, 0)
console.log(5)
// Output: 1 3 5 4 2
// promise then 和 catch
Promise.resolve()
.then(() => {
console.log(1)
})
.catch(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
// output: 1 3
Promise.resolve()
.then(() => {
console.log(1)
throw new Error('error1')
})
.catch(() => {
console.log(2)
})
.then(() => {
console.log(3)
})
// output: 1 2 3
Promise.resolve()
.then(() => {
console.log(1)
throw new Error('error1')
})
.catch(() => {
console.log(2)
})
.catch(() => {
console.log(3)
})
// output: 1 2
// async/await 顺序问题
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
// ounput : script start async1 start async2 promise1 script end async1 end promise2 setTimeout
webApi-dom
dom
: 文档对象模型,是HTML
和XML
的应用程序接口。dom
的作用:1. 使JavaScript
能够读取和操作HTML
文档的内容;2. 使JavaScript
能够操作CSS
样式。dom
常用 api:document.getElementById()
: 通过id
获取元素。document.getElementsByTagName()
: 通过标签名获取元素。document.getElementsByClassName()
: 通过类名获取元素。document.querySelector()
: 通过选择器获取元素。document.querySelectorAll()
: 通过选择器获取元素。document.createElement()
: 创建元素。document.createTextNode()
: 创建文本节点。document.createAttribute()
: 创建属性节点。document.appendChild()
: 添加子节点。document.removeChild()
: 删除子节点。document.replaceChild()
: 替换子节点。
attr
与property
区别相同:
attr
和property
都能获取到元素的属性值。不同:
attr
获取的是html
属性值,property
获取的是dom
对象的属性值。
减少
dom
操作: 1. 缓存dom
查询结果;2. 使用documentFragment
;3. 使用cloneNode
。dom
本质 :dom
是js
操作html
的接口,dom
本质是一个树形结构。重排与重绘:
- 重排:当
dom
的变化影响了元素的几何属性(宽和高)时,浏览器需要重新计算元素的几何属性,重新构建dom
树,这个过程称为重-排;
- 重排:当
- 重绘:当元素的几何属性(宽和高)发生变化时,浏览器会根据元素的新属性重新绘制元素,这个过程称为重绘。
重排一定会引起重绘,重绘不一定会引起重排。
减少重排与重绘:1. 使用
class
一次性修改样式;2. 将需要多次重排的元素,设置为绝对定位,使其脱离文档流,重排的时候不会影响到其他元素。
dom
事件流:事件捕获阶段 -> 事件目标阶段 -> 事件冒泡阶段。dom
事件流的三个阶段:- 事件捕获阶段:事件从
window
开始,向下传播到事件的目标元素。 - 事件目标阶段:事件到达目标元素。
- 事件冒泡阶段:事件从目标元素开始,向上冒泡到
window
。
- 事件捕获阶段:事件从
// dom 属性
// const div1 = document.getElementById('div1')
// console.log('div1', div1)
// const divList = document.getElementsByTagName('div') // 集合
// console.log('divList.length', divList.length)
// console.log('divList[1]', divList[1])
// const containerList = document.getElementsByClassName('container') // 集合
// console.log('containerList.length', containerList.length)
// console.log('containerList[1]', containerList[1])
// const pList = document.querySelectorAll('p')
// console.log('pList', pList)
// const pList = document.querySelectorAll('p')
// const p1 = pList[0]
// // property 形式
// p1.style.width = '100px'
// console.log( p1.style.width )
// p1.className = 'red'
// console.log( p1.className )
// console.log(p1.nodeName)
// console.log(p1.nodeType) // 1
// // attribute
// p1.setAttribute('data-name', 'imooc')
// console.log( p1.getAttribute('data-name') )
// p1.setAttribute('style', 'font-size: 50px;')
// console.log( p1.getAttribute('style') )
// dom 操作
const div1 = document.getElementById('div1')
const div2 = document.getElementById('div2')
// 新建节点
const newP = document.createElement('p')
newP.innerHTML = 'this is newP'
// 插入节点
div1.appendChild(newP)
// 移动节点
const p1 = document.getElementById('p1')
div2.appendChild(p1)
// 获取父元素
console.log(p1.parentNode)
// 获取子元素列表
const div1ChildNodes = div1.childNodes
console.log(div1.childNodes)
const div1ChildNodesP = Array.prototype.slice
.call(div1.childNodes)
.filter((child) => {
if (child.nodeType === 1) {
return true
}
return false
})
console.log('div1ChildNodesP', div1ChildNodesP)
div1.removeChild(div1ChildNodesP[0])
// 事件
// 通用的事件绑定函数
// function bindEvent(elem, type, fn) {
// elem.addEventListener(type, fn)
// }
function bindEvent(elem, type, selector, fn) {
if (fn == null) {
fn = selector
selector = null
}
elem.addEventListener(type, (event) => {
const target = event.target
if (selector) {
// 代理绑定
if (target.matches(selector)) {
fn.call(target, event)
}
} else {
// 普通绑定
fn.call(target, event)
}
})
}
// 普通绑定
const btn1 = document.getElementById('btn1')
bindEvent(btn1, 'click', function (event) {
// console.log(event.target) // 获取触发的元素
event.preventDefault() // 阻止默认行为
alert(this.innerHTML)
})
// 代理绑定
const div3 = document.getElementById('div3')
bindEvent(div3, 'click', 'a', function (event) {
event.preventDefault()
alert(this.innerHTML)
})
// const p1 = document.getElementById('p1')
// bindEvent(p1, 'click', event => {
// event.stopPropagation() // 阻止冒泡
// console.log('激活')
// })
// const body = document.body
// bindEvent(body, 'click', event => {
// console.log('取消')
// // console.log(event.target)
// })
// const div2 = document.getElementById('div2')
// bindEvent(div2, 'click', event => {
// console.log('div2 clicked')
// console.log(event.target)
// })
// ajax 封装
function ajax(url) {
const p = new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.onreadystatechange = function () {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText))
} else if (xhr.status === 404 || xhr.status === 500) {
reject(new Error('404 not found'))
}
}
}
xhr.send(null)
})
return p
}
const url = '/data/test.json'
ajax(url)
.then((res) => console.log(res))
.catch((err) => console.error(err))
webApi-bom
BOM
:Browser Object Model
,浏览器对象模型,提供了独立于内容而与浏览器窗口进行交互的对象结构,BOM
由多个对象组成,如window
、location
、navigator
、screen
、history
等。navigator
:浏览器信息navigator.userAgent
:浏览器信息字符串
screen
: 屏幕信息screen.width
screen.height
screen.colorDepth
location
:地址信息location.href
: 整个地址栏location.protocol
: 协议location.host
: 主机location.pathname
: 路径location.search
: 查询字符串location.hash
: 锚点location.reload()
: 刷新location.assign()
: 跳转location.replace()
: 替换
history
:历史记录history.back()
: 后退history.forward()
: 前进history.go()
: 前进或后退
安全
XSS
:跨站脚本攻击,指的是攻击者在网站上注入恶意的客户端代码,当用户浏览网页时,嵌入其中Web
页面中JavaScript
会被执行,从而达到攻击的目的。XSS
防范- 转义字符
CSP
:内容安全策略,是一种额外的安全层,用于检测并削弱某些特定类型的攻击,包括XSS
攻击。HttpOnly
:设置cookie
为HttpOnly
,HttpOnly
是微软对cookie
提出的一个安全的改进,使用HttpOnly
可以禁止Javascript
访问带有HttpOnly
属性的cookie
,可以防止XSS
攻击窃取cookie
。- 输入检测:对用户输入的内容进行检测,对于不符合要求的输入,直接过滤或者提示用户重新输入。
CSRF
:跨站请求伪造,攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息、购买商品、虚拟货币转账等。CSRF
防范Cookie
验证Referer
验证Token
验证
网站性能优化
让加载更快:
- 减少资源体积: 压缩代码
- 减少访问次数: 合并代码,ssr 服务器端渲染,缓存
- 使用更快的网络,CDN
让渲染更快:
- css 放在 head, js 放在 body 最下面
- 尽早开始执行 js,用 DOMContentLoaded 触发
- 图片懒加载
- 对 DOM 查询进行缓存
- 频繁 DOM 操作,合并到一起插入 DOM 结构
面试题
// 数组方法中 forEach、map、filter、reduce 的区别
// forEach:没有返回值,不会改变原数组
// map:返回一个新数组,不会改变原数组
// filter:返回一个新数组,不会改变原数组
// reduce:返回一个新数组,不会改变原数组
// 数组中纯函数的方法: map、filter、reduce、some、every、forEach
// 数组中非纯函数的方法:push、pop、shift、unshift、splice、sort、reverse
// 什么是纯函数: 相同的输入永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态
// 将url 参数解析为js 对象
function queryToObj() {
const obj = {};
// 获取url 参数 去掉?
const search = location.search.substr(1);
search.split('&').forEach((item) => {
const [key, value] = item.split('=');
obj[key] = value;
}
return obj;
}
// 手写数组 flatern 考虑多层级 拍平
function flatern(arr) {
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatern(cur) : cur);
}, []);
}
// 手写数组 map
Array.prototype.map = function (fn) {
const arr = [];
for (let i = 0; i < this.length; i++) {
arr.push(fn(this[i], i, this));
}
return arr;
};
// 手写数组 filter
Array.prototype.filter = function (fn) {
const arr = [];
for (let i = 0; i < this.length; i++) {
if (fn(this[i], i, this)) {
arr.push(this[i]);
}
}
return arr;
};
// 手写数组 reduce
Array.prototype.reduce = function (fn, init) {
let pre = init;
let i = 0;
if (init === undefined) {
pre = this[0];
i = 1;
}
for (; i < this.length; i++) {
pre = fn(pre, this[i], i, this);
}
return pre;
};
// 手写数组 forEach
Array.prototype.forEach = function (fn) {
for (let i = 0; i < this.length; i++) {
fn(this[i], i, this);
}
};
// 手写数组 some
Array.prototype.some = function (fn) {
for (let i = 0; i < this.length; i++) {
if (fn(this[i], i, this)) {
return true;
}
}
return false;
};
// reduce 用法
// 1. 数组求和
const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce((pre, cur) => {
return pre + cur;
}, 0);
// 2. 数组去重
const arr = [1, 2, 3, 4, 5, 1, 2, 3];
const newArr = arr.reduce((pre, cur) => {
if (!pre.includes(cur)) {
return pre.concat(cur);
}
return pre;
}, []);
// 3. 数组对象属性求和
const arr = [
{ name: '张三', age: 18 },
{ name: '李四', age: 19 },
{ name: '王五', age: 20 },
];
const sum = arr.reduce((pre, cur) => {
return pre + cur.age;
}, 0);
// 4. 数组对象属性分组
const arr = [
{ name: '张三', age: 18 },
{ name: '李四', age: 19 },
{ name: '王五', age: 20 },
];
const group = arr.reduce((pre, cur) => {
if (pre[cur.age]) {
pre[cur.age].push(cur);
} else {
pre[cur.age] = [cur];
}
return pre;
}, {});
// 5. 数组拍平
const arr = [1, 2, [3, 4], [5, 6, [7, 8]]];
const flatern = arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flatern(cur) : cur);
}, []);
// 节流: 一段时间内只执行一次,如果在这段时间内再次触发,则不执行
function throttle(fn, delay) {
let timer = null;
return function () {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
};
}
// 防抖: 一段时间内只执行一次,如果在这段时间内再次触发,则重新计时
function debounce(fn, delay) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
// 手写 new
function myNew(fn, ...args) {
const obj = Object.create(fn.prototype);
const result = fn.apply(obj, args);
return result instanceof Object ? result : obj;
}
// 手写 instanceof
function myInstanceof(left, right) {
let proto = left.__proto__;
while (true) {
if (proto === null) {
return false;
}
if (proto === right.prototype) {
return true;
}
proto = proto.__proto__;
}
}
// 手写 call
Function.prototype.myCall = function (context, ...args) {
context = context || window;
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
};
// 手写 apply
Function.prototype.myApply = function (context, args) {
context = context || window;
context.fn = this;
const result = context.fn(...args);
delete context.fn;
return result;
};
// 手写 bind
Function.prototype.myBind = function (context, ...args) {
const self = this;
return function (...args2) {
return self.apply(context, args.concat(args2));
};
};