JavaScript
var let const
var
- var声明的范围是函数作用域 变量会在函数退出时被销毁
- var存在变量声明提升,也就是把所用变量都拉到函数作用域的顶部
- 此外,反复多次var声明同一个变量也没有问题
let
- let声明的范围是块级作用域
- 同一个块作用域不能重复声明
const
- const用来声明不改变的常量 尝试修改会报错TypeError
- 也不允许重复声明 也是块作用域
变量和函数声明提升
- 对所有函数声明进行提升(除了函数表达式和箭头函数),引用类型的赋值
- 对变量进行提升,只声明,不赋值,值为
undefined
作用域 作用域链
- 规定变量和函数的可使用范围称作作用域
- 每个函数都有一个作用域链,查找变量或者函数时,需要从局部作用域到全局作用域依次查找,这些作用域的集合称作作用域链
new创建对象
new内部实现了什么
- 创建一个新对象
- 使新对象的
__proto__ 指向原函数的prototype - 改变this指向(指向新的obj)并执行该函数,执行结果保存起来作为result
- 判断执行函数的结果是不是null或Undefined,如果是则返回之前的新对象,如果不是则返回result
手写一个new
function myNew(fn, ...args) {
let obj = {}
obj.__proto__ = fn.prototype
let result = fn.apply(obj, args)
return result instanceof Object ? result : obj
}
字面量和new创建的对象和Object.create(null)创建的对象区别
- 字面量和new创建的对象会继承Object的方法和属性,他们的隐式原型会指向Object的显式原型
- 而Object.create(null)创建出来的对象原型为null,作为原型链的顶端,自然也没有继承Object的方法和属性
数据类型
简单数据类型(也称原始类型)
- Undefined
- Null
- Boolean
- Number
- String
- Symbol (ECMAScript6新增)
复杂数据类型
Object可以细分为Object、Function、Array、Date、RegExp
判断数据类型的几种方式
- typeof
typeof是一个操作符而不是一个函数,typeof null的值为Object
原因:javascript 数据在底层的存储是2进制的,存储的前三位标识类型,Object类型的前三位是000,而null所有数字都是000,所以typeof null 值为Object
- instanceof
操作符instanceof 用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上
- Object.prototype.toString.call()
Object.prototype.toString(o) 是 Object 的原型方法
- constructor
Symbol
symbol是一个ES6标准种新增的一种基本数据类型,每一个 symbol 的值都是唯一的,并且 symbol 类型的值可以作为对象的属性标识符使用,这也是 symbol 类型设计的目的。
用法
创建一个 symbol 的值需要使用 Symbol() 函数,而不能使用 new 命令。
let s1 = Symbol('sym');
由于生成的 symbol 是一个值而不是对象,所以不能为其添加属性
Symbol() 函数可以接受一个字符串作为参数,表示对该值的描述,因此即使定义 symbol 使用相同的参数互相之间也不是相同的:
let s1 = Symbol('sym');
let s2 = Symbol('sym');
s1 === s2 ;
数组
将类数组转化为数组
- 通过Array.from()方法来实现转化
Array构造函数有两个ES6新增的用于创建数组的静态方法:from()和of() 。from()用于将类数组结构转化为数组实例,而of()用于将一组参数转化为数组实例
Array.from(arrayLike);
console.log(Array.from("Matt"));
const a1 = [1, 2, 3, 4];
const a2 = Array.from(a1);
console.log(a2);
Array.from()可以接受3个参数 第一个参数是一个类数组对象,或者有一个length数字那个和可索引元素的结构。第二、第三个为可选参数,详见JS红宝书P139
- 通过call 调用数组的slice方法来实现转换
Array.prototype.slice.call(arrayLike);
- 通过call调用数组的splice方法来实现转换
Array.prototype.splice.call(arrayLike);
- 通过apply调用数组的concat方法来实现转换
Array.prototype.concat.apply([], arrayLike);
数组的方法
栈方法
push()接受任意数量的参数,并将他们添加到数组末尾,返回数组的最新长度
let colors = new Array();
let count = colors.push('red', 'green');
alert(count);
pop()方法用于删除数组的最后一项,同时减少数组的length值,返回被删除的项
队列方法
删除数组的第一项返回数组的一项,然后数组长度减1
在数组开头添加任意多个值,然后返回新的数组长度
排序方法
- reverse() 将数组元素反向排列
- sort()
默认情况下,sort()会按照升序重新排列数组,sort()会在每一项上调用String()转型函数,然后比较字符串来决定顺序,即使数组的元素都是数值,也会先把数组转换为字符串再比较、排序,但这在多数情况下是不合适的
为此,sort()可以接受一个比较函数,用于判断哪个值应该排在前面。
function compare(value1, value2){
if(value1 < value2){
return -1;
}else if (value1 > value2){
return 1;
}else {
return 0;
}
}
操作方法
可以在现有数组全部元素的基础上创建一个新的数组。它首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾,最后返回这个新构建的数组。如果参数不是数组,则直接把它们添加到结果数组的末尾。
不影响原数组,slice()用于创建一个包含原有数组中一个或多个元素的新数组。
slice()方法可以接受一个或两个参数:返回元素的开始索引和结束索引。如果只有一个参数,则sliec()会返回该索引到数组末尾的所有元素。
splice()的主要目的是在数组中间插入元素,但又3种不同的方式使用:
- 删除 需要给splice()传2个参数:要删除的第一个元素位置和要删除的元素数量 比如:splice(0, 2)
- 插入 需要给splice()传3个参数:开始位置、0(要删除的元素数量)、要插入的元素(可以任意多个) 比如:splice(2, 0, “red”, “green”)
- 替换 删除元素的同时在指定位置插入新元素,同样要传入3个参数:开始位置、要删除元素的数量和要插入的任意多个元素 比如:splice(2, 1, “red”, “green”)
迭代方法
forEach() 和 map() 的区别
forEach(): 对数组的每一项都运行传入的函数,没有返回值
map(): 对数组的每一项都运行传入的函数,返回由每次函数调用的结果构成的数组
数组去重 数组展平
var arr = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
let newArr2 = []
for (let i = 0; i < arr.length; i++) {
if (!newArr2.includes(arr[i])) {
newArr2.push(arr[i])
}
}
console.log(newArr2);
let arr2 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
for (let i = 0; i < arr2.length; i++) {
var item = arr2[i]
for (let j = i + 1; j < arr2.length; j++) {
var compare = arr2[j];
if (compare === item) {
arr2.splice(j, 1)
j--
}
}
}
console.log(arr2);
let arr3 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
let obj = {}
for (let i = 0; i < arr3.length; i++) {
let item = arr3[i]
if (obj[item]) {
arr3[i] = arr3[arr3.length - 1]
arr3.length--
i--
continue;
}
obj[item] = item
}
console.log(arr3);
console.log(obj);
let newArr1 = new Set(arr)
console.log([...newArr1]);
let arr4 = [1, 2, 1, 1, 1, 2, 3, 3, 3, 2]
newArr4 = arr4.reduce((prev, curr) => prev.includes(curr)? prev : [...prev,curr],[])
console.log(newArr4);
console.log(document);
rest参数
rest参数,形式为:…变量名。arguments对象不是数组,只是可以下标访问而已,而rest参数是一个真正的数组。
function f(a, b, ...args) {
}
原型和原型链
原型
每个函数都有 prototype 属性,指向原型对象,这个就是显式原型
通过函数实例化出来的对象有个_proto_ 属性,指向原型对象,这个就是隐式原型
原型有一个 constructor 属性,指回与之关联的构造函数。
原型链
原型链为ECMAScript的主要继承方式。其基本思想就是通过原型继承多个引用类型的属性和方法
js继承问题
原型继承
核心:将父类实例作为子类原型
优点:方法复用 由于方法定义在父类的原型上,复用了父类构造函数的方法
缺点:不能传参 子类实例共享了父类构造函数的引用属性 无法实现多继承
Parent.prototype.say = function () {
console.log('hello')
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
构造函数继承
核心:借用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类
优点:实例间独立
- 创建子类实例,可以像父类构造函数传参数
- 子类实例不共享父类构造函数的引用属性
- 可实现多继承(通过多个call或者apply继承多个父类)
缺点:
- 父类的方法不能复用 由于方法在父构造函数中定义,导致方法不能复用(因为每次创建子类实例都要创建一遍方法)
- 子类实例,继承不了父类原型上的属性。(因为没有用到原型)
function Parent(name) {
this.name = name;
this.arr = [1];
}
function Child(name, like) {
Parent.call(this, name, like)
this.like = like;
}
组合继承
核心:通过调??类构造函数,继承?类的属性并保留传参的优点;然后通过将?类实例作为?类原型,实现函数复?。
优点:
- 保留构造函数的优点:创建?类实例,可以向?类构造函数传参数。
- 保留原型链的优点:?类的?法定义在?类的原型对象上,可以实现?法复?
- 不共享?类的引?属性
缺点:
由于调?了2次?类的构造?法,会存在?份多余的?类实例属性
function Parent(name) {
this.name = name;
this.arr = [1];
}
Parent.prototype.say = function () {
console.log('hello')
}
function Child(name, like) {
Parent.call(this, name, like)
this.like = like;
}
Child.prototype = new Parent()
Child.prototype.constructor = Child
let boy1 = new Child('?红', 'apple')
let boy2 = new Child('?明', 'orange')
console.log(boy1.name, boy1.like);
console.log(boy1.say === boy2.say)
boy1.arr.push(2)
console.log(boy1.arr, boy2.arr);
寄?组合继承
function Parent(name) {
this.name = name;
this.arr = [1];
}
Parent.prototype.say = function () {
console.log('hello')
}
function Child(name, like) {
Parent.call(this, name, like)
this.like = like;
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
let boy1 = new Child('?红', 'apple')
let boy2 = new Child('?明', 'orange')
let p1 = new Parent('?爸爸')
注意: 这种? 法也要修复构造函数的
修复代码: Child.prototype.constructor = Child详解js继承 7
修复之后: console.log(boy1.constructor);
console.log(p1.constructor);
闭包 内存泄漏 垃圾回收机制
闭包
一个函数访问另一个函数内部的变量就形成闭包
作用:
- 实现封装,防止变量流入其他环境发生命名冲突
- 变量不会被销毁,保护函数内的变量安全
缺点:导致变量不会被垃圾回收机制回收,造成内存消耗
解决方法:在使用完变量后手动为它赋值为null
内存泄漏
不再用的内存没有被及时释放出来,导致该段内存无法被使用就是内存泄漏
JS垃圾回收机制
垃圾回收程序运行的时候,会标记内存中的所有变量,然后它会将所有在上下文中的变量,以及被在上下文中的变量引用的变量的标记去掉,在此之后还有标记的变量就是待删除的
其思路是每个值都记录它被引用的次数,当一个值的引用数为0时,就可以安全的收回其内存了,垃圾回收程序下次运行的时候就会释放引用数为0的值的内存
对象的深浅拷贝
浅拷贝
遍历赋值
function clone(obj){
var cloneObj = {}
for (var key in obj){
if (obj.hasOwnProperty(key)){
cloneObj[key] = obj[key]
}
}
return cloneObj
}
深拷贝
function deepClone(obj){
return JSON.parse(JSON.stringfy(obj))
}
function deepClone(obj){
if(!obj || typeof obj != "object") return obj
if(obj instanceof RegExp) return new RegExp(obj)
var cloneObj = new obj.constructor
for(var key in obj){
if(obj.hasOwnProperty(obj)){
cloneObj[key] = deepClone(obj[key])
}
}
return cloneObj
}
防抖和节流
防抖
function debounce(fn, delay){
var timer = null;
return function(){
if(timer){
clearTimeout(timer);
}
timer = setTimeout(fn, delay);
}
}
节流
function throttle(){
let timer = null;
return function() {
if(timer) return false;
timer = setTimeout(() => {
fn()
timer = null
}, delay)
}
}
AJAX 手写请求过程
var xhr = new XMLHttpRequest()
xhr.open("get", url, true)
xhr.send(null)
xhr.onreadystatechange = function(){
if (xhr.readyState == 4){
if ((xhr.status >= 200&& xhr.status < 300)||xhr.status == 304){
console.log(xhr.responseText)
}
else{
throw new Error("error")
}
}
}
跨域
JSONP
JSONP格式包含两个部分:回调和函数
例如 callback({ "name": "Nicholas" });
- JSONP通过同源策略涉及不到的"漏洞",也就是像 img 中的 src,link 标签的 href, script 的 src 都没有被同源策略限制到
- JSONP只能get请求
- 不安全,容易造成xss攻击
- 原因:去请求别的域,得到别的域返回的数据,万一是个脚本,那就注入到自己的代码里了
function addScriptTag(src){
var script = document.createElement("script")
script.setAttribute('type','text/javascript')
script.src = src
document.appendChild(script)
}
function callback(res){
console.log(res.message)
}
CORS(跨源资源共享)
(CORS,Cross-Origin Resource Sharing)定义了浏览器与服务器如何实现跨源通信。
CORS背后的基本思路就是使用自定义的HTTP头部允许浏览器和服务器相互了解,以确实请求或响应应该成功还是失败
有简单请求和非简单请求,满足一下条件就是简单请求
- 请求方法是HEAD、POST、GET
- 请求头只有Accept、AcceptLanguage、ContentType、ContentLanguage、Last-Event-Id
简单请求在发送是会添加一个额外的头部叫Origin,Origin头部包含发送请求的页面的源(协议、域名和端口),以便服务器确定是否为其提供响应
非简单请求
通过一种叫预检请求的服务器验证机制,在进行非简单请求时,会先向服务器发送一个“预检”请求,这个请求使用OPTIONS方法,在这个请求发送之后,服务器可以确定是否允许这种类型的请求。
通过预检后,浏览器接下来的每次请求就类似于简单请求了
nginx代理跨域
- nginx模拟一个虚拟服务器,因为服务器与服务器之间是不存在跨域的
- 发送数据时,客户端 ->nginx->服务端
- 返回数据时,服务端->nginx->客户端
箭头函数和普通函数区别
箭头函数是普通函数的简写,但是它不具备很多普通函数的特性
- this指向,箭头函数的this指向他定义时所在的对象,而不是调用时的对象
- 不会进行函数提升
- 没有arguments对象,不能使用arguments,如果要获取参数的话可以使用rest运算符
- 没有yield属性,不能作为生成器Generator使用
- 不能new
- 没有自己的this,不能调用call和apply
- 没有prototype,new关键字内部需要把新对象的
_proto_ 指向函数的prototype
call、apply、bind
当我们使用一个函数需要改变this 指向的时候才会用到call apply bind
区别
- 如果你要传递的参数不多,则可以使用
fn.call(thisObj, arg1, arg2 ...) - 如果你要传递的参数很多,则可以用数组将参数整理好调用
fn.apply(thisObj, [arg1, arg2 ...]) - 如果你想生成一个新的函数长期绑定某个函数给某个对象使用,则可以使用
const newFn = fn.bind(thisObj); newFn(arg1, arg2...) - call,apply,bind 不传参数自动绑定在 window
手写
Promise
promise是为了解决JS的什么问题?
解决了回调地狱的问题,增强了代码的可读性,所谓回调地狱就是指把函数作为参数层层嵌套请求,这样层层嵌套,人们称之为回调地狱,代码阅读性非常差。
Promise
Promise是异步编程的一种解决方案,是一个异步函数,主要是为了解决异步处理回调地狱(也就是循环嵌套的问题)而产生的,有三种状态,待定(pending)、兑现(fulfilled,有时候也称为“解决”,resolved)、拒绝(rejected)
let promise = new Promise((resolve, reject) => {
if ( ) {
resolve(success)
} else {
reject(error)
}
})
promise.then((success) => {
}).catch((error) => {
})
promise.all promise.race
Promise类提供两个将多个期约实例组合成一个期约的静态方法:Promise.all()和Promise.race()
Promise.all()静态方法创建的期约会在每个包含的期约全部解决之后再解决。这个静态方法接受一个可迭代对象,返回一个新期约
- 如果至少有一个包含的期约待定,则合成的期约也会待定。如果有一个包含的期约拒绝,则合成的期约也会拒绝
- 如果所有期约都成功解决,则合成期约的解决值就是所有包含期约解决值的数组
- 如果有期约拒绝,则第一个拒绝的期约会将自己的理由作为合成
Promise.race()返回一组集合中最先解决或拒绝的期约的镜像
Promise.all()和Promise.race区别
- Promise.all() 是数组里面所有的promise对象执行结束之后返回一个存储所有promise对象的结果
- Promise.race() 只会返回执行速度最快的那个promise对象返回的结果
async await
async用来修饰函数的声明,使用async修饰的函数会变成一个异步函数
await用来修饰函数的调用,被await修饰的函数必须返回一个promise异步对象,使用await修饰后,就会将promise异步对象转换成一个同步操作
Proxy
宏任务,微任务 事件循环EventLoop
宏任务一般是:包括整体代码script,setTimeout,setInterval。
微任务:Promise then catch finally,process.nextTick
setTimeout(() => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(() => {
console.log(3)
})
console.log(2)
JS中把所有任务分成了两类,一类是同步任务,一类是异步任务。同步任务指的是在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程,而是进入“任务队列”的任务,只有任务队列通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
具体来说,异步执行的机制如下:
- 所有同步任务都在主线程上执行,形成一个执行栈
- 主线程之外还有一个任务队列,只要异步任务有个结果,就在任务队列之中放置一个事件
- 一旦执行栈中的所有同步任务执行完毕,系统就会读取任务队列,看看里面有哪些事件。那些对应的异步任务于是结束等待状态,进入执行栈,开始执行
- 主线程不断重复上面的第三步。
模块化、common.js
为什么要使用模块化
- 防止命名冲突
- 更好的分离,按需加载
- 更好的复用性
- 更高的维护性
ES6和common.js的区别
commonjs 模块输出的是值的拷贝,而ES6输出的值是值的引用commonjs 是在运行时加载,是一个对象,ES6是在编译时加载,是一个代码块commonjs 的this指向当前模块,ES6的this指向undefined
JS性能优化方式
-
资源压缩与合并:减少文件体积,提高加载速度 -
减少http请求 -
利用浏览器缓存:提升二次访问的速度 -
使用CDN -
将CSS文件放在头部,JS文件放在尾部 -
图片懒加载 -
减少重绘和重排
|