JavaScript
原始类型与引用类型的区别
原始类型(值类型,基本类型):数值(Number
),字符串(String
),布尔(Boolean
),null
,undefined
引用类型:对象(Object)
区别:
- 赋值:原始类型赋(值),引用类型赋(引用)
// 原始类型赋值(真的赋值,内存空间1)
let str1 = 'hello'
let str2 = str1
str1 = 'world'
console.log(str1) //world
console.log(str2) //hello
//引用类型的赋值(赋引用,在内存空间1申明,在内存空间2赋值)
let stu1 = {name: 'xm'}
let stu2 = stu1
stu1.name = {name: 'xh'}
console.log(stu1.name) //xh
console.log(stu2.name) //xh
- 比较:原始类型比较的是值是否相等,引用类型比较的是引用是否指向统一对象
// 原始类型的比较
let str1 = 'hello'
let str2 = 'hello'
console.log(str1 === str2)//true
//引用类型的比较
let stu1 = {name: 'xm'}
let stu2 = {name: 'xm'}
console.log(stu1 === stu2)//false
let stu2 = stu1
console.log(stu1===stu2)//true
- 函数传参:原始类型作为参数,函数内的操作不影响实参的值。引用类型作为参数,函数内的操作会影响实参的值。
//原始类型传参
let fn1 = function(num) {
num = 100
}
let n = 10
fn1(n)
console.log(n) //10
//引用类型传参
let fn2 = function(arr) {
arr.push(10)
}
let a = [1, 2, 3]
fn2(a)
console.log(a) //[1,2,3,10]
typeof 和 instanceof
typeof:
- 是一元运算符,用于判断数据类型,返回值为字符串
- 分别为:string、Boolean、number、function、object、undefined、symbol
- typeof在判断null、array、object及函数实例(new+函数)时,得到的时object。这使得在判断这些数据类型的时候,得不到真实的数据类型。由此引出instanceof
instanceof: - instance中文翻译为实例,因此含义不言而喻,判读该对象是谁的实例,同时我们也就知道instanceof是对象运算符。
- instanceof运算符用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性。用于判断一个变量是否某个对象的实例
var a = new Array();
alert(a instanceof Array);//true
alert(a instanceof Object);//true,因为Array是object的子类
why?
- 每个函数function都有一个prototype,即原型。每个对象都有一个proto,为隐式原型。
每个对象都有一个proto属性,指向创建该对象的prototype。
function Foo(){}; var f1 =new Foo(); alert(f1 instanceof Foo) //true
instanceof 判断规则
- instanceof运算符的第一个变量是一个对象,暂称为A,第二个变量一般是函数,称为B。
- 沿着A的proto这条线来找,同时沿着B的prototype这条线来找
- 如果两条线能找到同一个引用,就是返回true,如果找到终点都没有重合就返回false
new的作用
- js中的new是来创建实例对象的。
- new开辟了一个新的空间来存储构造函数中初始化的数据,并将地址作为返回值返回
- 如果没有new,构造函数中的this指向全局变量,没有返回值,会显示undefined
实现的步骤:
- new会在内存中创建一个新的空对象
- new会让this指向这个新的对象
执行构造函数里面的代码
- 目的:给这个新对象加属性和方法
- new会返回这个新对象(所以构造函数里面不需要return)
防抖与节流
防抖:
- 用户触发事件过于频繁,只要最后一次事件的操作
- 防止重复调用,提高性能,避免卡死。
节流:
- 控制执行次数
- 作用:控制高频事件执行次数
this的各种情况
- 以函数形式调用时,this永远都是window
- 以方法的形式调用时,this是调用方法的对象
- 以构造函数的形式调用时,this是新创建的那个对象
- 使用call和apply调用时,this时指定的那个对象
箭头函数:箭头函数的this看外层是否有函数,
- 如果有,外层函数的this就是内部箭头函数的this。
- 如果没有就是window
特殊情况:通常意义上this指针指向为最后调用它的对象,但是需要注意一点的就是:
- 如果返回值是一个对象,那么this指向的就是那个返回的对象
- 如果返回值不是一个对象,那么this还是指向函数的实例
call apply bind
- call是一个方法,是函数的方法
- call可以调用函数
function fun(){
console.log('hello world')
}
fun.call()
- call可以改变函数中this的指向
function fun(){
console.log(this.name)
}
let dog={
name:'bob',
sayName(){
console.log('我是'+this.name)
},
eat(food1,food2){
console.log('我喜欢吃'+food1+food2)
}
}
let cat = {name:'tom'}
fun.call(cat)//tom
dog.sayName()//bob
dog.sayName.call(cat)//tom
dog.eat('骨头')
dog.eat.call(cat,'鱼')//call第一个参数是改变this的指向,后面的参数是要传的参数
apply
dog.eat.apply(cat,['鱼']) ```` ### == 和 === 的区别,什么时候用==? **==:**运算符称作相等,用来检测两个操作数是否相等,这里的相等定义非常宽松,可以进行类型转换,比如 console.log(123 == '123') //返回true
对于
string
,number
等基础类型,== 和 ===是有区别的。- ==是比较‘转化成统一类型后的值看此值是否相等,就是上述输出为何为true的原因
- ===如果类型不同,那么结果就是不等
- 同类型比较,直接进行 值 比较,两者结果一样。
- 对于
Array
,Object
等高级类型,== 和 ===是没有去别的 基础类型与高级类型,== 和 === 是有区别的
- ==,将高级转化为基础类型,进行值比较,
- 因为类型不同,=== 结果为false
js中垃圾回收机制是什么,常用的是哪种,怎么处理的?
js的垃圾回收机制是为了以防内存泄漏,内存泄漏的含义就是当已经不需要的某块内存是,这块内存还存在着,垃圾回收机制就是间歇的不定期的寻找到不再使用的变量,并释放掉它们所指向的内存
js中最常见的垃圾回收方式 是标记清除
工作原理:是当变量进入环境时,将这个变量标记为“进入环境”,当变量离开环境时,则将其标记为“离开环境”。标记“离开环境”的就回收内存
工作流程:- 垃圾回收期,在运行的时候会给存储在内存中的所有变量都加上标记。
- 去掉环境中的变量以及被环境中的变量引用的变量的标记
- 在被加上标记的会被视为准备删除的变量
垃圾回收器完成内存清除工作,销毁那些带标记的值并回收他们所占用的内存空间
for in 和 for of的区别
- for...in主要是为了遍历对象而生,不适用于遍历数组
- for...of 可以用来遍历数组,类型组对象,字符串,Set、Map及Generator对象
遍历输出不同
- for...in:只能获得对象的键名,不能获得键值
- for...of:允许遍历获得键值
var arr = ['red', 'green', 'blue'] //for in for (let item in arr) { console.log(item)//0 1 2 } //for of for (let item of arr) { console.log(item)//red green blue }
对于普通对象,没有部署原生的
iterator
接口,直接使用for...of会报错var obj = { 'name':'BOB', 'age':'22' } //for in for (let key in arr) { console.log(key)//name age } //for of for (let key of arr) { console.log(key)//Uncaught TypeError: obj is not iterable }
可以使用
Object.keys(obj)
方法将对象的键名生成一个数组,然后遍历这个数组for(let key of Object.keys(obj)){ console.log(key)//name age }
for...in不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。for...of则不会这样
let arr = [7,8,9] arr.set = 'world'//手动添加的键 Array.prototype = 'hello'//原型链上的键 for(let item in arr){ console.log(item)//0 1 2 set name } for(let item of arr){ console.log(item)//7 8 9 }
forEach 无法中途跳出,break命令或return命令都不能奏效
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9] arr.forEach(i => { if (i % 2 === 0) { return } console.log(i)//1 3 5 7 9 })
for...of可以与break,continue和return配合使用,跳出循环
for(let i of arr){ if(i % 2 === 0){ break } console.log(i)//1 }
无论是for...in还是for...of都不能遍历出Symbol类型的值,遍历Symbol类型的值需要用Object.getOwnPropertySymbols() 方法
let a = Symbol('a') let b = Symbol('a')
[a]: 'hello',
[b]: 'world',
c: 'es6',
d: 'dom'
}
for (let key in obj) {
console.log(key + '的值是' + obj[key])
}
let objSymbols = Object.getOwnPropertySymbols(obj)
console.log(objSymbols)
objSymbols.forEach(i => {
console.log(i, obj[i])
})
let keyArray = Reflect.ownKeys(obj)
console.log(keyArray)
设计模式
前端常见的设计模式主要有以下几种:
- 单例模式
这种设计模式的思想是确保一个类只有唯一实例,一般用于全局缓存,比如全局window,唯一登录浮窗等。
- 工厂模式
工厂模式是创建对象的常用设计模式,为了不暴露创建对象的具体逻辑,将逻辑封装在一个函数中,这个函数就称为一个工厂。本质上是一个负责生产对象实例的工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。
- 策略模式
策略模式的本意将算法的使用与算法的实现分离开来,避免多重判断调用哪些算法。适用于有多个判断分支的场景,如解决表单验证的问题。你可以创建一个validator对象,有一个validate()方法。这个方法被调用时不用区分具体的表单类型,它总是会返回同样的结果——一个没有通过验证的列表和错误信息
- 代理模式
代理模式是为其他对象提供一种代理,也就是当其他对象直接访问该对象时,如果开销较大,就可以通过这个代理层控制对该对象的访问。常见的使用场景为懒加载,合并http请求和缓存
- 观察者模式
也叫发布订阅模式,在这种模式中,一个订阅者订阅发布者,当一个特定的事件发生的时候,发布者会通知(调用)所有的订阅者。
- 模块模式
模块模式可以指定类想暴露的属性和方法,并且不会污染全局
- 构造函数模式
- 混合模式
构造函数和混合模式就是js中继承的两种实现方式,前者通过构造函数的形式定义类,通过new新增实例。而后者是将构造函数的引用属性和方法放到其原型上,子类是父类原型的一个实例。
前端模块化
说明:
模块化开发是一种管理方式,是一种生产方式,一种解决问题的方案,一个模块就是实现特定功能的文件,有了模块,我们就可以更方便地使用别人的代码,想要什么功能,就加载什么模块,但是模块化开发需要遵循一定的规范,否则就都乱套了,因此,才有了后来大家熟悉的AMD规范,CMD规范,以及ES6自带的模块化规范。
模块化带来的好处:
- 解决命名冲突
- 提供复用性
- 提高代码可维护性
- 灵活架构,焦点分离,方便模块间组合,分解
- 多人协作互不干扰
注意:
- CommonJS规范主要用于服务端编程,加载模块是同步的,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD,CMD解决方案
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅
- CMD规范和AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM打包,模块的加载逻辑偏重
- ES6在语言标准的层面上,实现了模块功能, 而且实现的相当简单,完全可以取代CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。
前端工程化
前端工程化是指使用软件工程的技术和方法来进行前端的开发流程,技术,工具,经验等规范化,标准化,其主要目的是为了提高效率和降低成本,既提高开发过程中的开发效率,减少不必要的重复工作时间。
前端工程化里的工程指软件工程,和我们一般说的工程是两个完全不同的概念
- 工程是个很泛泛的概念,甚至可以认为建了一个git仓库就相当于建了一个工程
- 软件工程的定义:应用计算机科学理论和技术以及工程管理原则和方法,按预算和进度,实现满足用户要求的软件产品的定义,开发,和维护的工程或进行研究的学科
前端工程化就是为了让前端开发能够“自成体系”,个人认为主要从模块化,组件化,规范化,自动化考虑
模块化
- 简单说模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。
JS的模块化
在ES6之前,JavaScript一直没有模块系统,这对开发大型复杂的前端工程造成了巨大的障碍。对此社区制定了一些模块加载方案,如CommonJS、AMD和CMD等。
现在ES6已经在语言层面上规定了模块系统,完全可以取代现有的CommonJS和AMD规范,而且使用起来相当简洁,并且有静态加载的特性。- 用++Webpack + Babel++将所有模块打包成一个文件同步加载,也可以搭乘多个chunk异步加载
- 用++System+Babel++主要是分模块异步加载
- 用浏览器的)
- 异步脚本(和defer功能类似,区别在于不会严格按照script标签顺序执行脚本,也就是说脚本2可能先于脚本1执行。脚本都会在onload事件前执行,但可能会在 DOMContentLoaded 事件触发前后执行。 )
- 注意:defer和async都只适用于外部脚本
webpack
webpack是一个打包模块化js的工具,可以通过loader转换文件,通过plugin扩展功能。
核心概念
- entry:一个可执行模块或者库的入口。
- chunk:多个文件组成一个代码块。可以将可执行的模块和他所依赖的模块组合成一个chunk,这是打包。(打包文件生成的一大堆代码,实际上就是一个自执行函数,仅传入一个参数为 modules,且该对象为一个数组,该函数的作用就是管理模块,它的内部定义了两个主要的对象 installedModules 对象(被当作字典使用,key 是模块的 id,value 是代表模块状态和导出的一个对象)和 __webpack_require__(moduleId) 函数对象。
- loader:文件转换器。例如把es6转为es5,scss转为css等
- plugin:扩展webpack功能的插件。在webpack构建的生命周期节点上加入扩展hook,添加功能。
构建流程
- Webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:
- 初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;
- 开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译
- 确定入口:根据配置中的 entry 找出所有的入口文件;
- 编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;
- 完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;
- 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;
- 输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。
- 在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果。
webpack的热更新是如何做到的?说明其原理
webpack的热更新又称热替换(Hot Module Replacement),缩写为HMR。 这个机制可以做到不用刷新浏览器而将新变更的模块替换掉旧的模块。
- 第一步,在 webpack 的 watch 模式下,文件系统中某一个文件发生修改,webpack 监听到文件变化,根据配置文件对模块重新编译打包,并将打包后的代码通过简单的 JavaScript 对象保存在内存中
- 第二步是 webpack-dev-server 和 webpack 之间的接口交互,而在这一步,主要是 dev-server 的中间件 webpack-dev-middleware 和 webpack 之间的交互,webpack-dev-middleware 调用 webpack 暴露的 API对代码变化进行监控,并且告诉 webpack,将代码打包到内存中
- 第三步是 webpack-dev-server 对文件变化的一个监控,这一步不同于第一步,并不是监控代码变化重新打包。当我们在配置文件中配置了devServer.watchContentBase 为 true 的时候,Server 会监听这些配置文件夹中静态文件的变化,变化后会通知浏览器端对应用进行 live reload。注意,这儿是浏览器刷新,和 HMR 是两个概念
- 第四步也是 webpack-dev-server 代码的工作,该步骤主要是通过 sockjs(webpack-dev-server 的依赖)在浏览器端和服务端之间建立一个 websocket 长连接,将 webpack 编译打包的各个阶段的状态信息告知浏览器端,同时也包括第三步中 Server 监听静态文件变化的信息。浏览器端根据这些 socket 消息进行不同的操作。当然服务端传递的最主要信息还是新模块的 hash 值,后面的步骤根据这一 hash 值来进行模块热替换
- webpack-dev-server/client 端并不能够请求更新的代码,也不会执行热更模块操作,而把这些工作又交回给了 webpack,webpack/hot/dev-server 的工作就是根据 webpack-dev-server/client 传给它的信息以及 dev-server 的配置决定是刷新浏览器呢还是进行模块热更新。当然如果仅仅是刷新浏览器,也就没有后面那些步骤了
- HotModuleReplacement.runtime 是客户端 HMR 的中枢,它接收到上一步传递给他的新模块的 hash 值,它通过 JsonpMainTemplate.runtime 向 server 端发送 Ajax 请求,服务端返回一个 json,该 json 包含了所有要更新的模块的 hash 值,获取到更新列表后,该模块再次通过 jsonp 请求,获取到最新的模块代码
- 而第 10 步是决定 HMR 成功与否的关键步骤,在该步骤中,HotModulePlugin 将会对新旧模块进行对比,决定是否更新模块,在决定更新模块后,检查模块之间的依赖关系,更新模块的同时更新模块间的依赖引用
- 最后一步,当 HMR 失败后,回退到 live reload 操作,也就是进行浏览器刷新来获取最新打包代码
如何利用webpack来优化前端性能
- 压缩代码。删除多余的代码、注释、简化代码的写法等等方式。可以利用webpack的UglifyJsPlugin和ParallelUglifyPlugin来压缩JS文件, 利用cssnano(css-loader?minimize)来压缩css。使用webpack4,打包项目使用production模式,会自动开启代码压缩。
- 利用CDN加速。在构建过程中,将引用的静态资源路径修改为CDN上对应的路径。可以利用webpack对于output参数和各loader的publicPath参数来修改资源路径
- 删除死代码(Tree Shaking)。将代码中永远不会走到的片段删除掉。可以通过在启动webpack时追加参数--optimize-minimize来实现或者使用es6模块开启删除死代码。
- 优化图片,对于小图可以使用 base64 的方式写入文件中
- 按照路由拆分代码,实现按需加载,提取公共代码。
- 给打包出来的文件名添加哈希,实现浏览器缓存文件
如何提高webpack的构建速度
- 多入口的情况下,使用commonsChunkPlugin来提取公共代码;
- 通过externals配置来提取常用库;
- 使用happypack实现多线程加速编译
- 使用webpack-uglify-parallel来提升uglifyPlugin的压缩速度。原理上webpack-uglify-parallel采用多核并行压缩来提升压缩速度;
使用tree-shaking和scope hoisting来剔除多余代码
Webpack是什么? Webpack与Grunt、Gulp有什么不同?
- 首先我们先回答这样的问题,这三者没什么可比性的。
- grunt和gulp在早期比较流行,属于前端工具类,主要优化前端工作流程。比如自动刷新页面、combo、压缩css、js、css预编译等等
- 现在webpack相对来说比较主流,属于预编译模块的方案,不需要在浏览器中加载解释器。另外,你在本地直接写JS,不管是 AMD / CMD / ES6 风格的模块化,它都能认识,并且编译成浏览器认识的JS
- grunt和gulp是基于任务和流(Task、Stream)的。类似jQuery,找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程
- webpack是基于入口的。会自动地递归解析入口所需要加载的所有资源文件,然后用不同的Loader来处理不同的文件,用Plugin来扩展webpack功能
- 从构建思路来说,gulp和grunt需要开发者将整个前端构建过程拆分成多个 Task,并合理控制所有 Task的调用关系 webpack需要开发者找到入口,并需要清楚对于不同的资源应该使用什么Loader做何种解析和加工
总结
- gulp,grunt是web构建工具
- webpack是模块化方案
- gulp,grunt是基于任务和流
- webpack基于入口文件
执行上下文
- 执行上下文(Execution Context): 函数执行前进行的准备工作(也称执行上下文环境)
- 运行JavaScript代码时,当代码执行进入一个环境时,就会为该环境创建一个执行上下文,它会在你运行代码前做一些准备工作,如确定作用域,创建局部变量对象等。
分类:
- 全局执行上下文
- 函数执行上下文
- eval函数执行上下文(不推荐使用)
分析:
- JavaScript运行时首先会进入全局环境,对应会生成全局上下文。程序代码中基本都会存在函数,那么调用函数,就会进入函数执行环境,对应就会生成该函数的执行上下文。
- 因为JS是"单线程"! "单线程"! "单线程"! 就是同个时间段只能做一件任务,完成之后才可以继续下一个任务。
- 函数编程中,代码中会声明多个函数,对应的执行上下文也会存在多个。在JavaScript中,通过栈的存取方式来管理执行上下文,我们可称其为执行栈,或函数调用栈(Call Stack)。
- 执行栈,或函数调用栈(Call Stack)。
- 程序执行进入一个执行环境时,它的执行上下文就会被创建,并被推入执行栈中(入栈);程序执行完成时,它的执行上下文就会被销毁,并从栈顶被推出(出栈),控制权交由下一个执行上下文。
- 因为JS执行中最先进入全局环境,所以处于"栈底的永远是全局环境的执行上下文"。而处于"栈顶的是当前正在执行函数的执行上下文",当函数调用完成后,它就会从栈顶被推出(理想的情况下,闭包会阻止该操作,闭包后续文章深入详解)。
- "全局环境只有一个,对应的全局执行上下文也只有一个,只有当页面被关闭之后它才会从执行栈中被推出,否则一直存在于栈底。"
函数的 原型(prototype)
任何一个js对象,都有一个原型对象,它可以使用自己原型对象上的所有属性和方法
let cat = { name:'tom' } cat.__proto__.eat=function(){ console.log('吃鱼') } cat.eat()//吃鱼
通过构造函数prototype属性拿到原型
function Cat(name,age){ this.name=name this.age=age } let cat = new Cat('tom',2) Cat.prototype.eat=function(){ console.log('吃鱼') } cat.eat()
- 原型对象的作用
可以给任何一个对象,通过原型来添加方法
显式原型和隐式原型
- 每个函数function都有一个prototype,即显式原型(属性)
- 每个实例对象都有一个__proto__ ,可称为隐式原型(属性)
- 对象的隐式原型的值为其对应构造函数的显式原型的值
- 内存结构
总结
- 函数的prototype属性:在定义函数时自动添加的,默认值是一个空Object对象
- 对象的__proto__属性:创建对象时自动添加的,默认值为构造函数的prototype属性值
- 程序员能直接操作显式原型, 但不能直接操作隐式原型(ES6之前)
//定义构造函数
function Fn(){ //内部语句:this.prototype = {}
}
//1.每个函数function都有一个prototype,即显式原型,默认指向一个空的Object对象
console.log(Fn.prototype)
//2.每个实例对象都有一个__proto__ ,可称为隐式原型
var fn = new Fn() //内部语句:this.__proto__=Fn.prototype
console.log(fn.__proto__)
//3.对象的隐式原型的值为其对应构造函数的显式原型的值
console.log(Fn.prototype===fn.__proto__) //true
//给原型添加方法
Fn.prototype.test = function(){
console.log('test')
}
//通过实例调用原型的方法
fn.test()
原型链 及 属性问题
原型链(图解)
访问一个对象的属性时,
- 先在自身属性中查找,找到返回
- 如果没有,再沿着__proto__这条链向上查找,找到返回
- 如果最终没找到,返回undefined
- 别名:隐式原型链
- 作用:查找对象的属性(方法)
- 构造函数/原型/实体对象的关系(图解)
- 构造函数/原型/实体对象的关系(图解)
function Fn(){
this.test1=function(){
console.log('test1()')
}
}
Fn.prototype.test2=function(){
console.log('test2()')
}
var fn = new Fun()
fn.test1()
fn.test2()
console.log(fn.toString())
fn.test3()
- 函数的显示原型指向的对象:默认是空的Object实例对象(但Object不满足)
console.log(Fn.prototype instanceof Object) //true
console.log(Object.prototype instanceof Object) //false
console.log(Function.prototype instanceof Object) //true
- 所有函数都是Function的实例(包含Function)
console.log(Function.__proto__===Function.prototype) //true
- Objec的原型对象是原型链尽头
console.log(Object.prototype.__proto__) //null
属性问题
- 读取对象的属性值时:会自动到原型链中查找
- 设置对象的属性值时:不会查找原型链,如果当前对象中没有此属性,直接添加此属性并设置其值
- 方法一般定义在原型中,属性一般通过构造函数定义在对象本身上
function Fn(){
}
Fn.prototype.a = 'xxx'
var fn1 = new Fn()
console.log(fn1.a)
var fn2 = new Fn()
fn2.a = 'yyy'
console.log(fn1.a)
属性放自身,方法放原型
function Person(name,age){
this.name = name
this.age = age
}
Person.prototype.setName = function(name){
this.name = name
}
var p1 = new Person('Tom',12)
p1.setName('Bob')
console.log(p1) //Bob 12
var p2 = new Person('jack',12)
p2.setName('Cat')
console.log(p2) //Cat 12
console.log(p1.__proto__===p2.__proto__) //true
作用域与作用域链
理解
- 就是一块“地盘”,一个码段所在的区域
- 它是静态的(相对于上下文对象),在编写代码时就确定了
分类
- 全局作用域
- 函数作用域
- 没有块作用域(ES6有了)
作用
- 隔离变量,不同作用域下同名变量不会有冲突
//没块作用域
if (true) {
var c = 3
}
console.log(c)
var a = 10,
b=2
function fn(x) {
var a = 100,
c = 300;
console.log('fn()',a,b,c,x)
function bar(x) {
var a = 1000,
d=400
console.log('bar',a,b,c,d,x)
}
bar(100)
bar(200)
}
fn(10)
作用域与执行上下文区别
区别1
- 全局作用域之外,每个函数都会创建自己的作用域,作用域在函数定义时就已经确定了。而不是在函数调用时
- 全局执行上下文环境是在全局作用域确定之后,js代码马上执行之前创建
- 函数执行上下文是在函数调用时,函数体代码执行之前创建
区别二
- 作用域是静态的,只要函数定义好了就一直存在,且不会再变化
- 执行上下文是动态的,调用函数时创建,函数调用结束时就会自动释放
联系
- 上下文环境(对象)是从属于所在的作用域
- 全局上下文环境==>全局作用域
- 函数上下文环境==>对应的函数作用域
作用域链
理解
- 多个上下级关系的作用域形成的链,它的方向时从下向上的(从内到外)
- 查找变量时就是沿着作用域链来查找的
查找一个变量的查找规则
- 在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入2
- 在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入3
- 再次执行2的相同操作,知道全局作用域,如果还找不到就抛出找不到的异常
var a = 1
function fn1(){
var b = 2
function fn2(){
var c = 3
console.log(c)
console.log(b)
console.log(a)
console.log(d)
}
fn2()
}
fn1()
闭包的理解
什么是闭包:
- 各种专业文献的闭包定义都非常抽象,我的理解是: 闭包就是能够读取其他函数内部变量的函数。
- 由于在javascript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。
- 所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
闭包的用处: - 闭包可以用在许多地方。它的最大用处有两个,
- 一个是前面提到的可以读取函数内部的变量,
- 另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。
注意事项: - 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
- 闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
promise
- ES6中一个非常好用和重要的特性
- Promise是异步编程的一种方案
什么时候用?
- 一般情况下是有异步操作时,使用Promise对这个异步操作进行封装
- Promise需传入俩个参数,resolve(成功时调用),reject(失败时调用)
new Promise((resolve, reject) => {
setTimeout(() => {
// 成功的时候调用resolve
// resolve('hello world')
// 失败的时候调用reject
reject('error message')
}, 1000)
}).then((data) => {
// 1.100行处理的代码
console.log(data);
console.log(data);
console.log(data);
console.log(data);
console.log(data);
console.log(data);
}).catch((err) => {
console.log(err)
}) //输出 error message
Promise三种状态
- 当开发中有异步操作时,就可以给异步操作包装一个Promise
异步操作之后会有三种状态
pending
:等待状态,比如正在进行网络请求,或者定时器没有到时间fulfill
:满足状态,当我们主动回调了resolve
时,就处于该状态,并且会回调.then()
reject
:拒绝状态,当我们主动回调了reject
时,就处于该状态,并且会回调.catch()
new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('hlwd')
reject('error')
},1000)
}).then(data => {
console.log(data);
},err => {
console.log(err)
})
Promise的链式调用
new Promise((resolve, reject) => {
// 第一次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
}).then(() => {
// 第一次拿到结果的处理代码
console.log('hellowd')
console.log('hellowd')
console.log('hellowd')
console.log('hellowd')
console.log('hellowd')
console.log('hellowd')
return new Promise((resolve, reject) => {
// 第二次网络请求的代码
setTimeout(() => {
resolve()
}, 1000)
})
}).then(() => {
// 第二次拿到结果的代码
console.log('hellow')
console.log('hellow')
console.log('hellow')
console.log('hellow')
console.log('hellow')
console.log('hellow')
return new Promise((resolve, reject) => {
// 第三次网络请求的代码
setTimeout(() => {
resolve()
})
},1000)
}).then(() => {
// 第三次拿到结果的代码
console.log('hellowd')
console.log('hello')
console.log('hello')
console.log('hello')
console.log('hello')
console.log('hello')
})
- 简化代码方式:
// 网络请求:aaa -> 自己处理(十行)
// 处理:aaa111 -> 自己处理(十行)
// 处理:aaa111222 -> 自己处理
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then((res) => {
// 1.自己处理10行代码
console.log(res, '第一次的十行处理代码');
//2.对结果进行第一次处理
return new Promise((resolve) => {
resolve(res + '111')
})
}).then((res) => {
console.log(res, '第二次的十行处理代码');
return new Promise((resolve) => {
resolve(res + '222')
})
}).then((res) => {
console.log(res, '第三次的10行处理代码');
})
// SIMPLE WRITTEN
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then((res) => {
// 1.自己处理10行代码
console.log(res, '第一次的十行处理代码');
//2.对结果进行第一次处理
return Promise.resolve(res + '111')
}).then((res) => {
console.log(res, '第二次的十行处理代码');
return Promise.resolve(res + '222')
}).then((res) => {
console.log(res, '第三次的10行处理代码');
})
// SIMPLE WRITTEN 2
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then((res) => {
// 1.自己处理10行代码
console.log(res, '第一次的十行处理代码');
//2.对结果进行第一次处理
return res + '111'
}).then((res) => {
console.log(res, '第二次的十行处理代码');
return res + '222'
}).then((res) => {
console.log(res, '第三次的10行处理代码');
})
//报错
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('aaa')
}, 1000)
}).then((res) => {
// 1.自己处理10行代码
console.log(res, '第一次的十行处理代码');
//2.对结果进行第一次处理
return Promise.reject('报错')
//or
//throw '报错'
}).then((res) => {
console.log(res, '第二次的十行处理代码');
return Promise.resolve(res + '222')
}).then((res) => {
console.log(res, '第三次的10行处理代码');
}).catch((eerr) => {
console.log(eerr);
})
Promise的all方法
- 加入某一需求要发送两次请求才能完成
Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result')
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('result2')
}, 1000)
})
]).then(results => {
console.log(results);
})
深拷贝 与 浅拷贝
- 深拷贝只是从源数据中拷贝一份出来进行操作,而不是改变源数据,改变源数据的是浅拷贝
- 原生js方法slice,concat都不是真正意义上的深拷贝,都仅只适用于一维数组,拷贝的属性不够彻底
- 实现js深拷贝可以通过JSON.parse(JSON.stringify()),递归实现
拷贝顾名思义就是复制,内存中分为栈内存和堆内存两大区域,所谓深浅拷贝主要是对js引用类型数据进行拷贝一份,浅拷贝就是引用类型数据相互赋值之后,例obj1=obj2;如果后面修改obj1或者obj2,这个时候数据是会进行相应的变化的,因为在内存中引用类型数据是存储在堆内存中,堆内存中存放的是引用类型的值,同时会有一个指针地址指向栈内存,两个引用类型数据地址一样,如果其中一个发生变化另外一个都会有影响,而深拷贝则不会,深拷贝是会在堆内存中重新开辟一块空间进行存放
基本类型复制:
var a = 1
var b = a//复制
console.log(b)//1
a = 2//改变a的值
console.log(b)//1
console.log(a)//2
a,b都是属于基本类型,基本类型的复制时不会影响对方的,因为基本类型时每一次创建变量都会在栈内存中开辟一块内存,用来存放值,所以基本类型进行复制是不会对另外一个变量有影响的
js浅拷贝:
var arr1 = ['red','green']
var arr2 = arr1//复制
console.log(arr2)//['red','green']
arr1.push('black')//改变color值
console.log(arr2)//['red','green','blcak']
console.log(arr1)//['red','green','black']
此例子是数组的浅拷贝,通过上面的知识我们可以看到数组是引用类型数据,引用类型数据复制是会进行相互影响的,我们看到arr1.push('black')添加了一个新的子项,因为上面 var arr2 = arr1这行代码是将两个引用类型数据的地址指针都指向了同一块堆内存区域,所以不管是arr1还是arr2修改,任何一个改动,两个数组都是会互相产生影响的,上面的那种直接赋值的复制方式就是常说的引用类型的浅拷贝
slice:
var arr1 = ['red','green']
var arr2 = arr1.slice(0)//复制
console.log(arr2)//['red','green']
arr1.push('black')//改变color值
console.log(arr2)//['red','green']
console.log(arr1)//['red',;'green','black']
js数组原生方法slice会返回一个新的数组,该代码乍一看会以为是深拷贝,因为arr2和arr1相互复制牵引,而当arr1调用了push方法添加了新数组子项的时候,arr2没有发生变化,是的,这是符合深拷贝的特性,但是拷贝的不够彻底,还不能算是真正意义上的深拷贝,所以slice只能被称为浅拷贝,slice方法只适用于一维数组的拷贝,在二维数组中就会破绽百出
//二维数组
var arr1 = [1,2,3,['1','2','3']]
var arr2 = arr1.slice(0)
arr1[3][0]=0
console.log(arr1)//[1,2,3,['0','2','3']]
console.log(arr2)//[1,2,3,['0','2','3']]
这是一二维数组,当我们在arr13里面进行更改arr1的值的时候,我们发现arr1、arr2两个数组的值都发生了变化,所以事实证明slice不是深拷贝
concat:
//一维数组
var arr1 = ['red','green']
var arr2 = arr1.concat()//复制
console.log(arr2)//['red','green']
arr1.push('black')//改变color值
console.log(arr2)//['red','green']
console.log(arr1)//['red','green','black']
//二维数组
var arr1 = [1,2,3,['1','2','3']]
var arr2 = arr1.concat()
arr1[3][0] = 0
console.log(arr1)//[1,2,3,['0','2','3']]
console.log(arr2)//[1,2,3,['0','2','3']]
concat方法在一维数组中是不会影响源数组的数据的,而在二维数组中concat表现和slice是一样的
js深拷贝:
js数组中实现深拷贝的方法有多种,比如JSON.parse(JSON.stringify())和递归都是可以实现数组和对象的深拷贝的
var arr1 = ['red','green']
var arr2 = JSON.parse(JSON.stringify(arr1))//复制
console.log(arr2)//['red','green']
arr1.push('black')//改变color的值
console.log(arr2)//['red','green']
console.log(arr1)//['red','green','black']
事件循环
js的运行机制
- 所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
- 主线程之外,还存在"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
- 一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
- 主线程不断重复上面的第三步
- 概括即是: 调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了,这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取-执行的操作
- 一个事件循环中有一个或者是多个任务队列
事件循环是什么
- 主线程从"任务队列"中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查microtask队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有microtask。然后再进入下一个循环去任务队列中取下一个任务执行。
详细说明
- 选择当前要执行的宏任务队列,选择一个最先进入任务队列的宏任务,如果没有宏任务可以选择,则会跳转至microtask的执行步骤。
- 将事件循环的当前运行宏任务设置为已选择的宏任务。
- 运行宏任务。
- 将事件循环的当前运行任务设置为null。
- 将运行完的宏任务从宏任务队列中移除。
- microtasks步骤:进入microtask检查点。
- 更新界面渲染。
- 需要注意的是:当前执行栈执行完毕时会立刻先处理所有微任务队列中的事件, 然后再去宏任务队列中取出一个事件。同一次事件循环中, 微任务永远在宏任务之前执行。
- 为什么要事件循环
因为 JavaScript 是单线程的。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。为了协调事件(event),用户交互(user interaction),脚本(script),渲染(rendering),网络(networking)等,用户代理(user agent)必须使用事件循环(event loops)。
事件冒泡和捕获
事件流:
描述的是从页面接收事件的顺序。
冒泡
IE提出的事件流叫做事件冒泡,即事件开始时由最具体的元素接受,然后逐级向上传播到较为不具体的节点
捕获
网景公司提出的事件流叫做事件捕获流。事件捕获流的思想是不太具体的DOM节点应该更早接收到事件,而最具体的节点应该最后接收到事件
event
welcome
hello
world
- document>html>body>div>h5
- 并且分别在obj1,obj2绑定了一个点击事件,由于addEvevntListener第三个参数设置为false,所以页面在冒泡阶段处理绑定事件(为true为捕获阶段)
点击文字welcome时,弹出hello。
此时就只触发了绑定在obj1上的点击事件。- 具体冒泡实现过程如下:
- welcome 属于文本节点,点击后,开始从文本节点查找,
- 当前文本节点没有绑定点击事件,继续向上找,
- 找到父级(id为obj1的div),有绑定的点击事件,执行,
- 再向上找,body,没有绑定点击事件,再到html,document,都没再有绑定的点击事件,
- 好,整个冒泡过程结束。
- 点击文字hello时,先弹出world,再弹出hello。
- 单机world时,弹出hello,冒泡过程和第二种情况类似。
宏任务和微任务
宏任务特征:有明确的异步任务需要执行和回调,需要其他异步线程支持
微任务特征:没有明确的异步任务需要执行,只有回调,不需要其他同步线程支持
宏任务:setTimeout,setInterval,Dom事件,Ajax
微任务:promise,async/await
执行顺序:微任务=>>Dom渲染=>>宏任务
数据类型检测
typeof:
- 直接在计算机底层基于数据类型的值(二进制)进行检测
- typeof null 的输出是 ‘object'
- 对象存储在计算机中,都是以000开始的二进制存储,null也是,所以检测出来是对象,但null并不是对象,可以理解为bug
instanceof:
- 检测当前实例是否属于这个类
- 底层机制:只要当前类出现在实例的原型链上,结果都是true
- 由于我们可以肆意的改动原型的指向,多亿检测出来的结果是不准的
- 不能检测基本数据类型
constructor:
- 用起来看似比instanceof还好用一些(支持基本类型)
- constructor可以随便改,所以也不准
let arr = []
console.log(arr.constructor === Array) //true
let n = 1
console.log(n.constructor === Number) //true
Number.prototype.constructor = ‘aa’
console.log(n.constructor === Number) //false
Object.prototype.toString.call([value])
- 标准检测数据类型的办法:Object.prototype.toString不是转换成字符串,是返回当前实例所属类的信息
http缓存
强缓存:
当从浏览器第一次访问一个网站,浏览器会向服务器发送请求,服务器返回资源,如果服务器觉得返回的资源是要被缓存的,js css img,就会在响应头,response header 中增加一个 cache-control,可以设置max-age:3153600 单位是s,这样浏览器就会缓存,如果设置nocache,就不会用本地缓存策略
下一次再访问,如果没有过期,就直接从缓存拿资源,就会很快,不用再发http
第一次访问:200 状态码
协商缓存(对比缓存):
是一种服务端缓存策略
从浏览器向服务器发请求获取资源,
如果用了该策略去访问,服务器就会返回资源和资源标识,并且可以在浏览器把资源存到本地缓存
后续请求,不仅发送请求还会发送资源标识(last modified在响应头,if modified since 在请求头、etag)
这样服务器就会判断,当前请求的资源在缓存本地的版本和服务器是否一致,如果是最新的资源,服务器就会返回304状态码,不需要发送资源文件,并直接从缓存里拿资源
如果不是最新资源,服务器就会返回200状态码,同时把最新的资源和新的资源标识都返回,相当于完整的请求,这就是协商缓存
资源标识:last modified 和 etag
Etag优先使用(字符串形式)
last modified在响应头,
if modified since 在请求头、
文件如果每隔一段时间都重复生成,但内容相同,last-modified是修改时间会每次返回资源文件,即使内容相同
但Etag可以判断出文件内容相同,就会返回304
webpack的loader和plugins的区别
Loader:加载器
文件加载器,运行在NodeJS中,能够加载资源文件,并对这些文件进行一些处理,比如编译,压缩等,最终一起打包到指定文件中,将A文件运行编译形成B文件,这里操作的是文件,比如将A.scss或A.less转变为B.css,单纯的文件转换过程
- css-loader和style-loader模块是为了打包css
- babel-loader和babel-core模块是为了把ES6代码转换为ES5
- url-loader和file-loader是把图片进行打包的
plugin:插件
webpack运行的生命周期会广播出许多事件,plugin可以监听这些事件,在合适的时机通过webpack提供的api改变输出结果。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。
插件可以携带参数,所以在plugins属性传入new实例
plugins:[
//对html模板进行处理,生成对应的html,引入需要的资源模块
new HtmlWebpackPlugin({
template:'./index.html',//模板文件,即需要打包和拷贝到build目录下的html文件
filename:'index.html',//目标html文件
chunks:['useperson'],//对应加载的资源,即html文件需要引入的js模块
inject:true//资源加入到底部,把模块引入到html文件的底部
})
]
loader和plugin的区别:
- loader用来加载某些资源文件,因为webpack只能理解js和json文件,对于其他如css,图片,或者其他的语法集,比如jsx,coffee,没有办法加载。这就需要对应的loader将资源转化,加载进来,字面意思也能看出,loader用于加载,它作用于一个个文件上。
- plugin用于扩展webpack的功能,目的在于解决loader无法实现的其他事,直接作用于webpack,扩展了它的功能。当然loader也是变相扩展了webpack,但是它只专注于转化文件(transform)领域。而plugin功能更加丰富,而不仅局限于资源的加载
webpack对图片的处理
- 由于webpack中只有js类型文件才能被识别并且打包,其他类型的文件如css,图片等,需要特定的loader进行加载打包
webpack中打包图片需要用到file-loader或者url-loader加载器,这两个加载器功能基本一样,但是有区别
- file-loader:根据配置项复制到的资源到构建之后的文件夹,并能更改对应的链接
- url-loader:包含file-loader的全部功能,并且能根据配置将符合配置的文件转换成Base64方式引入,将小体积的图片Base64引入项目可以减少http请求,也是前端常用的优化方式
以url-loader为例
- 安装url-loader(npm install url-loader --save-dev)
- 配置选项(webpack.config.js 文件中的 module.rules 数组中添加 url-loader 的相关配置)
module:{
rules:[
{
test: /\.(png|jpg|gif|svg)$/,
use: 'url-loader'
}
]
}
- 要想打包后图片名称不变,并且能够添加到指定目录下,可以在 rules 中添加一个 options 属性
rules:[
{
test: /\.(png|jpg|gif|svg)$/,
use:[{
loader: 'url-loader',
options: {
name: 'images/[name].[ext]'
},
}]
}
]
- 优化图片(压缩图片大小)安装image-webpack-loader
- 修改webpack.config.js配置
{
test: /\.(png|jpg|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10000,
name: 'images/[name].[ext]'
}
},
{
loader:'image-webpack-loader',
options: {
bypassOnDebug: true,
}
}
]
}
nginx的反向代理和webpack的代理区别
- node.js反向代理,替换的只是原请求地址的域名,直接把标识符之前的内容直接替换,不是标识符的所有内容
- nginx反向代理,nginx是要根据后台的实际情况来处理,有可能是直接把标识符及之前的内容都替换掉,也有可能是只替换标识符之前的内容。
跨域
跨域问题其实就是浏览器同源策略导致的
同源策略
- 用于限制一个origin的文档或者它加载的脚本如何能与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介----MDN
什么才是同源
- 只有当协议(protocol),域名(domain),port(端口)三者一致才是同源
如何解决跨域
- CORS
- 跨域资源共享(CORS)是一种机制,它使用额外的HTTP头来告诉浏览器 让运行在一个origin(domain)上的web应用被准许访问来自不同源服务器上的指定的资源。当一个资源与该资源本身所在的服务器不同的域,协议或端口请求一个资源时,资源会发起一个跨域HTTP请求
- 浏览器支持情况IE>9,opera>12,FF>3.5。比这些版本小的用JSONP
简单请求:
- 不会触发CORS预检请求。(该术语不属于Fetch规范)。若请求满足下述所有条件,则该请求可视为 ‘简单请求’
- 情况一:使用以下方法(除了以下请求外的都是非简单请求)
- GET
- HEAD
- POST
- 情况二:人为设置以下集合外的请求头
- Accept
- Accept-Language
- Content-Language
- Content-Type (需要注意额外的限制)
- DPR
- Downlink
- Save-Data
- Viewport-Width
- Width
- 情况三:Content-Type的值仅限于下列三者之一:(例如 application/json 为非简单请求)
- text/plain
- multipart/form-data
- application/x-www-form-urlencoded
cors中的cookie问题:
- 想要传递cookie需要满足三个条件
- web请求设置withCredentials(这里默认情况存在跨域请求,浏览器是不带cookie的)
- Access-Control-Allow-Credentials 为 true
- Access-Control-Allow-Origin为非 *
- 这里请求的方式,在 chrome 中是能看到返回值的,但是只要不满足以上其一,浏览器会报错,获取不到返回值。
cors请求头:
- Access-Control-Allow-Origin:指示请求的资源能共享给哪些域
可以是具体的域名或者*表示所有域。
* Access-Control-Allow-Credentials:指示当请求的凭证标记为 true 时
是否响应该请求。
* Access-Control-Allow-Headers:用在对预请求的响应中,
指示实际的请求中可以使用哪些 HTTP 头。
* Access-Control-Allow-Methods:指定对预请求的响应中,
哪些 HTTP 方法允许访问请求的资源。
* Access-Control-Expose-Headers:指示哪些 HTTP 头的名称能在响应中列出。 * Access-Control-Max-Age:指示预请求的结果能被缓存多久。 * Access-Control-Request-Headers:用于发起一个预请求,
告知服务器正式请求会使用那些 HTTP 头。
* Access-Control-Request-Method:用于发起一个预请求,
告知服务器正式请求会使用哪一种 HTTP 请求方法。
* Origin:指示获取资源的请求是从什么域发起的
- Node正向代理
- 代理思路为,利用服务端请求不会跨域的特性,让接口和当前站点同域
cli 工具中的代理
- 在cli 工具中中配置proxy来快速获取接口代理的能力
- Nginx反向代理
- 通过反向代理的方式,(这里需要自定义一个域名)这里就是保证我当前域,能获取到静态资源和接口,不关心是怎么获取的。
- JSPON
主要利用了script标签没有跨域限制这个特性来完成的(仅支持GET方法)
- 流程
- 前端定义解析函数(例如jspCallback=function(){...})
- 通过params形式包装请求参数,并且声明执行函数(cb=jspCallback)
- 后端获取前端声明的执行函数(jspCallback),并以带上参数并调用函数的方式传递给后端
window.name+Iframe
- window对象的name属性是一个很特别的属性,当该window的location变化,然后重新加载,它的name属性可以依然保持不变
- 通过iframe的src属性由外域转向本地域,跨域数据既由iframe的window.name从外域传递到本地域。这个就巧妙绕过了浏览器的跨域访问限制,但同时又是安全操作
- 浏览器开启跨域(非必要情况)
- 注意事项: 因为浏览器是众多 web 页面入口。我们是否也可以像客户端那种,就是用一个单独的专门宿主浏览器,来打开调试我们的开发页面。例如这里以 chrome canary 为例,这个是我专门调试页面的浏览器,不会用它来访问其他 web url。因此它也相对于安全一些。当然这个方式,只限于当你真的被跨域折磨地崩溃的时候才建议使用以下。使用后,请以正常的方式将他打开,以免你不小心用这个模式干了其他的事。
windows环境
- 安装目录
- .\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx