赞
踩
JS
是什么类型的语言?程序在执行之前需要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。
程序执行效率高,依赖编译器,跨平台性差些。
C
、C++
都是编译型语言。
程序不需要编译,程序在运行时才翻译成机器语言,每执 行一次都要翻译一次。
解释型语言执行效率较低,且不能脱离解释器运行,但它的跨平台型比较容易,只需提供特定解释器即可。
python
、JS
都是解释型语言。
JS
中有哪些强制类型转换和隐式类型转换?String()
Number()
Boolean()
parseInt()
parseFloat()
+ string
转为数字a + " "
转为字符串!var
转为布尔值eg
:var a = {name: 'yuhua'}
变量存储情况代码区域 Code Segment
;a
放入 栈(Stack):本地变量、指针
;{name: 'yuhua'}
放入HeapTotal(堆):对象,闭包
。symbol
symbol
作为一个对象的键名时,如何获取?不能获取 symbol
键:
for in
与 for of
循环遍历中,不会获取 symbol
键;Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
方法获取不到 symbol
键;能获取 symbol
键:
Object.getOwnPropertySymbols()
方法可以获取,返回一个数组;Reflect.ownKeys()
可以获取所有的键名,包括 symbol
键symbol
的类型转换string
const symbolKey = Symbol(123)
String(symbolKey) // "Symbol(123)"
symbolKey.toString() // "Symbol(123)"
Boolean(symbolKey) // true
Number(symbolKey)
Uncaught TypeError: Cannot convert a Symbol value to a number
at Number (<anonymous>)
at <anonymous>:1:1
b = Object(symbolKey) Symbol {Symbol(123)} description: "123" __proto__: Symbol constructor: ƒ Symbol() description: "123" toString: ƒ toString() valueOf: ƒ valueOf() Symbol(Symbol.toPrimitive): ƒ [Symbol.toPrimitive]() Symbol(Symbol.toStringTag): "Symbol" get description: ƒ description() __proto__: Object [[PrimitiveValue]]: Symbol(123) typeof b // "object" b.constructor() // Symbol() b instanceof Symbol // true b instanceof Object // true Object.prototype.toString.call(b) // "[object Symbol]"
eval()
let funcStr = "function test(value){alert(value)}";
let test = eval("(false || "+funcStr+")");
test("函数能够执行");
new Function()
function add(a, b) {
return a + b;
}
//等价于
var add = new Function ('a', 'b', 'return a + b');
let funcStr = "function test(value){alert(value)}";
let funcTest = new Function('return '+funcStr);
funcTest()("函数也能够执行")
null
和 undefined
的区别Null
null
表示一个"无"的对象,转为数值为 0
;Number(null)
为 0
5 + null
位 5
Undefined
undefined
;undefined
;undefined
;undefined
;Number(undefined)
为 NaN
;5 + undefined
为 NaN
。typeof
和 instanceof
的区别typeof
表示对某个变量类型的检测,基本数据类型除了 null
都能正常的显示为对应的类型,引用类型除了函数会显示为 function
外,其他的都是会显示为 object
;instanceof
用于检测某个构造函数的原型对象在不在某个对象的原型链上。typeof
对 null
的错误显示这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。
instanceof
Object.getPrototypeOf():Object.getPrototypeOf()
方法返回指定对象的原型(内部[[ Prototype
]]属性的值)。
function myInstance (left, right) {
let proto = Object.getPrototypeOf(left) // Object.getPrototypeOf() 方法返回指定对象的原型(内部[[Prototype]]属性的值)。
while(true) {
if (proto === null) return false
if (proto === right.prototype) return true
proto = Object.getPrototypeOf(proto)
}
}
验证
myInstance([], Object) //true
myInstance(Map, Object) //true
myInstance(new Map(), Object) //true
myInstance(Map, Function) //true
myInstance(class {}, Function) //true
myInstance(1, Number) //true
myInstance('1', String) //true
this
this
对于函数而言指向最后调用函数的那个对象,是函数运行时内部自动生成的一个内部对象,只能在函数内部使用;对于全局来说,this
指向 window
。
this
是在什么时候确定的?函数调用时,指向最后调用的那个对象
call
、apply
、bind
三者的区别三个函数的作用都是将函数绑定到上下文中,用来改变函数中 this
的指向;三者的不同点在于语法的不同。
fun.call(thisArg[, arg1[, arg2[, ...]]])
fun.apply(thisArg, [argsArray])
var bindFn = fun.bind(thisArg[, arg1[, arg2[, ...]]])
bindFn()
apply
和 call
的区别是 call
方法接受的是若干个参数列表,而 apply
接收的是一个包含多个参数的数组。
而 bind()
方法创建一个新的函数, 当被调用时,将其 this
关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。
const name = 'window' const sayName = function (param) { console.log('my name is:' + this.name + ',my param is ' + param) } sayName('window param') //my name is:window,my param is window param const callObj = { name: 'call' } sayName.call(callObj, 'call param') //my name is:call,my param is call param const applyObj = { name: 'apply' } sayName.apply(applyObj, ['apply param']) //my name is:apply,my param is apply param const bindObj = { name: 'bind' } const bindFn = sayName.bind(bindObj, 'bind param') bindFn() //my name is:bind,my param is bind param
this
默认绑定到 window
。this
隐式绑定到该直接对象。window
。显式绑定:通过 call()
、apply()
、bind()
方法把对象绑定到 this
上,叫做显式绑定。new
绑定:如果函数或者方法调用之前带有关键字 new
,它就构成构造函数调用。对于 this
绑定来说,称为 new
绑定。this
this
,所以需要通过查找作用域链来确定 this
的值,这就意味着如果箭头函数被非箭头函数包含,this
绑定的就是最近一层非箭头函数的 this
。arguments
对象,但是可以访问外围函数的 arguments
对象。new
关键字调用,同样也没有 new.target
值和原型。call
、apply
和 bind
?call
Function.prototype.myCall = function (thisArg, ...args) {
const fn = Symbol('fn') // 声明一个独有的 symbol 属性,防止 fn 覆盖已有属性
thisArg = thisArg || window // 若没有 this 传入,则绑定 window 对象
thisArg[fn] = this // this 指向调用 call 的对象,即我们要改变 this 指向的函数
const result = thisArg[fn](...args) // 执行当前函数
delete thisArg[fn] // 删除我们声明的 fn
return result // 返回函数执行结果
}
apply
Function.prototype.myApply = function (thisArg, args) {
const fn = Symbol('fn') // 声明一个 symbol
thisArg = thisArg || window // 设置 thisArg
thisArg[fn] = this // this 指向改变
const result = thisArg[fn](...args) // 执行函数
delete thisArg[fn] // 删除 fn
return result // 返回结果
}
bind
Function.prototype.myBind = function (thisArg, ...args) {
const self = this
const fbound = function () {
self.apply(this instanceof self ? this : thisArg,args.concat(Array.prototype.slice.call(arguments)))
}
fbound.prototype = Object.create(self.prototype)
return fbound
}
this
指向obj0.obj.test()
const a = 1
function test () {
console.log(this.a)
}
const obj = {
a: 2,
test
}
const obj0 = {
a: 3,
obj
}
obj0.obj.test() // 2
testcopy()
var a = 1
function test () {
console.log(this.a)
}
const obj = {
a: 2,
test
}
const testCopy = obj.test
testCopy() // 1
// this 指向是在函数执行时确定
setTimeout
中var a = 1
function test () {
console.log(this.a)
}
const obj = {
a: 2,
test
}
setTimeout(obj.test) // 1
// this 指向是在函数执行时确定
JS
模块化IIFE
自执行函数AMD
使用 requireJS
来编写模块化(依赖必须提前声明好。)CMD
使用 seaJS
来编写模块化(支持动态引入依赖文件。)CommonJS
nodeJs
中自带的模块化UMD
兼容 AMD
、CommonJS
语法webpack(require.ensure)
:webpack 2.x
版本中的代码分割ES Modules
: ES6
引入的模块化,支持 import
来引入另一个 js
script
标签 type="module"
AMD
和 CMD
的区别
AMD
和CMD
最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块
AMD
推崇依赖前置,在定义模块的时候就要声明其依赖的模块CMD
推崇就近依赖,只有在用到某个模块的时候再去 require
CommonJS
规范的特点CommonJS
输出的是值的拷贝,模块内部再次改变也不会影响这个值(引用类型和基本类型有区别)ES6 modules
规范有什么特点export
import
export ... from ...
来达到一个中转的效果export
和 import
命令处于模块顶层,不能位于作用域内,处于代码块中,没法做静态优化,违背了 ES6
模块的设计初衷import
有提升效果,会提升到整个模块的头部,首先执行Babel
会把 export/import
转化为 exports/require
的形式,所以可以使用 exports
和 import
CommonJS
和 ES6 Modules
规范的区别CommonJS
模块是运行时加载,ES6Modules
是编译时加载CommonJS
输出值的拷贝,ES6Modules
输出值的引用(模块内部改变会影响引用)CommonJS
导入模块可以是一个表达式(是使用 require()
引入),ES6Modules
导入只能是字符串CommonJS
中 this 指向当前模块,ES6Modules
中 this
指向 undefined
ES6Modules
中没有 arguments
、require
、module
、exports
、__filename
、__dirname
这些顶层变量AMD
和 CMD
支持异步加载模块
node require(X)
引入的处理顺序是什么样的?X
是内置模块,返回该模块,不再继续执行;X
以 './'、'/'、'../'
开头,将根据 X
所在的父模块,确定 X
的绝对路径:X
当成文件,依次查找,存在,返回该文件,不再继续执行;X
当成目录,依次查找目录下的文件,存在,返回该文件,不再继续执行;X
不带有路径:X
所在的父模块,确定 X
可能的安装目录X
当成文件名或者目录名加载not found
错误有个 a.js
和 b.js
两个文件,互相引用
CommonJS
{
id: '...',
exports: { ... },
loaded: true, parent: null, filename: '', children: [], paths: []
}
CommonJS
的一个模块,就是一个脚本文件。require
命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。以后需要用到这个模块的时候,就会到 exports
属性上面取值。即使再次执行 require
命令,也不会再次执行该模块,而是到缓存之中取值。
CommonJS
重要特性是加载时执行,脚本代码在 require
时,全部执行。
CommonJS
的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
// a.js
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
//b.js
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
a.js
脚本先输出一个 done
变量,然后加载另一个脚本文件 b.js
。注意,此时 a.js
代码就停在这里,等待 b.js
执行完毕,再往下执行。b.js
执行到第二行,就会去加载 a.js
,这时,就发生了"循环加载"。系统会去 a.js
模块对应对象的 exports
属性取值,可是因为 a.js
还没有执行完,从 exports
属性只能取回已经执行的部分,而不是最后的值。a.js
已经执行的部分,只有一行。exports.done = false;
b.js
来说,它从 a.js
只输入一个变量 done
,值为 false
。b.js
接着往下执行,等到全部执行完毕,再把执行权交还给 a.js
。于是,a.js
接着往下执行,直到执行完毕。我们写一个脚本 main.js
,并运行,验证这个过程。// main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
// 运行
// 在 b.js 之中,a.done = false
// b.js 执行完毕
// 在 a.js 之中,b.done = true
// a.js 执行完毕
// 在 main.js 之中, a.done=true, b.done=true
b.js
之中,a.js
没有执行完毕,只执行了第一行。二是,main.js
执行到第二行时,不会再次执行 b.js
,而是输出缓存的 b.js
的执行结果,即它的第四行。ES6
ES6
模块的运行机制与 CommonJS
不一样,它遇到模块加载命令 import
时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。
ES6
模块是动态引用,不存在缓存值的问题,而且模块里面的变量,绑定其所在的模块。ES6
模块不会缓存运行结果,而是动态地去被加载的模块取值,以及变量总是绑定其所在的模块。
ES6
根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
// even.js
import { odd } from './odd'
export var counter = 0;
export function even(n) {
counter++;
return n == 0 || odd(n - 1);
}
// odd.js
import { even } from './even';
export function odd(n) {
return n != 0 && even(n - 1);
}
按照 CommonJS
规范,是没法加载的,是会报错的,但是 ES6
就可以执行。
之所以能够执行,原因就在于 ES6
加载的变量,都是动态引用其所在的模块。只要引用是存在的,代码就能执行。
$ babel-node
> import * as m from './even.js';
> m.even(10);
true
> m.counter
6
> m.even(20)
true
> m.counter
17
上面代码中,参数 n
从 10
变为 0
的过程中,foo()
一共会执行 6
次,所以变量 counter
等于 6
。第二次调用 even()
时,参数 n
从 20
变为 0
,foo()
一共会执行 11
次,加上前面的 6
次,所以变量 counter
等于17
。
JS
事件事件委托/事件代理:一般来说就是通过事件冒泡把一个元素的响应事件的函数代理到它的父层或者更外层元素上。
缺点:
focus/blur
)document
、window
、html
、body
的层级关系window > document > html > body
window
是 BOM
的核心对象,一方面用来获取或者设置浏览器的属性和行为,一方面作为一个全局对象;document
是一个跟文档相关的对象,拥有一些操作文档内容的功能;html
元素 和 document
元素对象是属于 html
文档的 DOM
对象。addEventListener
函数的第三个参数是什么?boolean
时:true
时是捕获,为 false
时是冒泡。Object
时:capture
: Boolean
,表示 listener
会在该类型的事件捕获阶段传播到该 EventTarget
时触发。once
: Boolean
,表示 listener
在添加之后最多只调用一次。如果是 true
, listener
会在其被调用之后自动移除。passive
:Boolean
,设置为 true
时,表示 listener
永远不会调用 preventDefault()
。如果 listener
仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。mozSystemGroup
:只能在 XBL
或者是 Firefox' chrome
使用,这是个 Boolean
,表示 listener
被添加到 system group
。冒泡:当给某个元素绑定了事件之后,这个事件会依次在它的父级元素中被触发;
捕获:从上层向下层传递,与冒泡相反。
<!-- 会依次执行 button li ul -->
<ul onclick="alert('ul')">
<li onclick="alert('li')">
<button onclick="alert('button')">点击</button>
</li>
</ul>
<script>
window.addEventListener('click', function (e) {
alert('window')
})
document.addEventListener('click', function (e) {
alert('document')
})
</script>
冒泡:button -> li -> ul -> document -> window
捕获:window -> document -> ul -> li -> button
onblur
onfoucs
onmouseenter
onmouseleave
Event
customEvent
document.createEvent('customEventName')
和 initEvent()
Event
let myEvent = new Event('my_event_name')
customEvent
let myEvent = new CustomEvent('my_event_name', {
detail: {
// 需要传递的参数
// 在监听的回调函数中获取到:event.detail
}
})
document.createEvent('CustomEvent')
和initEvent()
let myEvent = document.createEvent('CustomEvent')
myEvent.initEvent(
// event_name 是事件名
// canBubble 是否冒泡
// cancelable 是否可以取消默认行为
)
dom.addEventListener('my_custom_name', function(e) {})
dispatchEvent(myEvent)
// 1. let myEvent = new Event('myEvent'); // 2. let myEvent = new CustomEvent('myEvent', { detail: { name: 'lindaidai' } }) // 3. let myEvent = document.createEvent('CustomEvent'); myEvent.initEvent('myEvent', true, true) let btn = document.getElementsByTagName('button')[0] btn.addEventListener('myEvent', function (e) { console.log(e) console.log(e.detail) }) setTimeout(() => { btn.dispatchEvent(myEvent) }, 2000)
JS
内部函数和闭包MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
简单来说:能够读取其他函数内部变量的函数就是闭包。
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i)
}, 1000)
})(i)
}
一般来说在一个函数内部定义另外一个函数,这样的函数就是内部函数。
DOM
操作不是,即使是 1byte
的内存,也叫内存泄露。
不是,着一般是无限递归函数调用,导致栈内存溢出。
堆区。栈区不会泄露
大多数情况下,后果不是很严重。但是过多的 DOM
操作会使网页执行速度变慢。
仍然存在,直到浏览器关闭。
EventLoop
的执行过程EventLoop
的执行过程script
作为一个宏任务进行执行;UI
线程渲染工作;web worker
任务,有则执行;requestAnimationFrame
因为 rAF
是官方推荐的用来做一些流畅动画所应该使用的 API
,做动画不可避免的会去更改 DOM
,而如果在渲染之后再去更改 DOM
,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这显然是不合理的。
rAF
在浏览器决定渲染之前给你最后一个机会去改变 DOM
属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择。
requestIdleCallback
requestIdleCallback
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
50ms
可以确保用户在无感知的延迟下得到回应。
EventLoop
循环注意点requestAnimationFrame
在重新渲染屏幕之前执行,非常适合用来做动画。requestIdleCallback
在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度,如果你一定要它在某个时间内执行,请使用 timeout
参数。resize
和 scroll
事件其实自带节流,它只在 Event Loop
的渲染阶段去派发事件到 EventTarget
上。for
循环和 setTimeout
在 for
循环中加入 setTimeout
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
var
改成 let
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i)
}, 1000)
})(i)
}
for
循环改成 forEach
循环[1,2,3,4].forEach(item => {
setTimeout(() => {
console.log(item)
}, 1000)
})
setTimeout
传参for (var i = 0; i < arr.length; i++) {
setTimeout((i) => {
console.log(arr[i])
}, 1000, i)
}
for (var i = 0; i< 10; i++){
setTimeout(console.log(i),1000);
}
JS
中的 let
、const
、var
JS
中有几种定义变量的方法?let
const
var
class
import
function
let
、const
、var
有什么区别?var | let | const |
---|---|---|
没有块级作用域 | 有块级作用域 | 有块级作用域 |
声明全局变量在 window 下 | (全局属性下) 全局变量不在全局属性下 | 全局变量不在全局属性下 |
重定义变量不会报错 | 会报错 | 会报错 |
声明变量 | 声明变量 | 声明一个常量 |
存在变量提升 | 不存在变量提升 | 不存在变量提升 |
声明之后随时赋值 | 声明之后随时赋值 | 声明之后立即赋值 |
const
定义常量可不可以修改?const
定义基础类型是不可以修改的;const
定义引用类型是可以修改引用类型里面的值。const
定义引用类型也不能改变它的值该怎么做?Object.freeze
;proxy/Object.defineProperty
);configurable
、writable
属性。ES5
的情况下实现 let
和 const
?let
可以通过自执行函数。
const
可以通过 Object.defineProperty()
实现,设置 writable
。
JS
数组ES6
新增数组方法Array.from()
、Array.of()
、copyWithin()
、find()
、findIndex()
、fill()
、entries()
、keys()
、values()
、includes()
。
ES5
新增数组方法forEach()
、map()
、filter()
、some()
、every()
、indexOf()
、lastIndexOf()
、reduce()
、reduceRight()
。
copyWithin()
、fill()
、pop()
、push()
、reverse()
、shift()
、sort()
、splice()
。
some
和 every
有什么区别?从中文含义能看出来,some
是某些,every
是每一个,它们都返回一个 Boolean
值。
用时基本上一样,因为 js
里面没有数组类型,数组其实也是一个对象,key
和 value
。
for
循环;ES6
Set
去重;filter/includes/indexOf
;Map
、Object
去重。for
循环和 forEach
的性能哪个更好一点?for
循环的性能更好
for
循环没有任何额外的函数调用栈和上下文;forEach
不是普通的 for
循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能。sort
排序是按照什么方式来排序的?默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16
代码单元值序列时构建的。
reduce
递归实现join
和 split
实现flat
方法toString
和 split
实现v
;v
的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和 v
有路径相通的顶点都被访问;const depth = (node) => { let stack = [] let nodes = [] if (node) { stack.push(node) while (stack.length) { //每次取最后一个 let item = stack.pop() let children = item.children || [] nodes.push(item) //判断children的长度 for (let i = children.length - 1; i >= 0; i--) { stack.push(children[i]) } } } return nodes }
const breadth = (node) => { let nodes = [] let stack = [] if (node) { stack.push(node) while (stack.length) { //取第一个 let item = stack.shift() let children = item.children || [] nodes.push(item) for (let i = 0; i < children.length; i++) { stack.push(children[i]) } } } return nodes }
reduce
Array.prototype.myReduce = function (fn, init) {
if (!init && this.length === 0) { // 如果数组长度为0
return this
}
let start = 1, pre = this[0]; // 从数组第二个开始下标为1
if (init !== undefined) { // 如果 init 字段存在,从第一个开始,下标为 0
start = 0;
pre = init;
}
for (let i = start; i < this.length; i++) { // 循环
let current = this[i]
pre = fn.call(this, pre, current, i, this) // 把每次的 reduce 的值返回
}
return pre
}
function disOrder2 (arr) {
for (let i = 0; i < arr.length; i++) { // 遍历
const randomIndex = Math.floor(Math.random() * ary.length) // 生成随机数
swap(arr, i, randomIndex)
}
}
function swap(arr, i, _i) { // 交换
const tem = arr[i]
arr[i] = arr[_i]
arr[_i] = tem
}
arr = [1,2,3,4,5,6,7,8]
disOrder(arr)
console.log(arr)
num.replace(/(\d)(?=(\d{3})+(\.|$))/g, "$1,")
function formatNumber(num) { if (!num) return ""; let [int, float] = num.split("."); let intArr = int.split(""); let result = []; let i = 0; while (intArr.length) { if (i !== 0 && i % 3 === 0) { result.unshift(intArr.pop() + ","); } else { result.unshift(intArr.pop()); } i++; } return result.join("") + "." + (float ? float : ""); }
map
、find
、every
、some
、forEach
等方法的第二个参数是干什么的?arr.every(callback(element[, index[, array]])[, thisArg])
thisArg
callback
时使用的 this
值。for in
和 for of
有什么区别?比较 | for in | for of |
---|---|---|
不同点 | 可以遍历普通对象 遍历出数组的原型对象 可以遍历出数组自身属性 遍历出来的值是 key 不可以遍历 map/set 不可以迭代 generators IE 支持 | 不能遍历普通对象 不会遍历出原型对象 不会遍历自身属性 遍历出来的值是 value 可以遍历 map/set 可以迭代 generators IE 不支持 |
相同点 | 可以遍历数组 可以 break 中断遍历 | 可以遍历数组 可以 break 中断遍历 |
Promise
sleep
函数(延迟函数)通过 promise
和 setTimeout
来简单实现
/**
* 延迟函数
* @param {Number} time 时间
*/
function sleep (time = 1500) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time)
})
}
promise
构造函数、then
方法、catch
方法、finally
方法哪个异步哪个同步?promise
构造函数是同步执行的,then
、catch
和 finally
方法是异步执行的。
promise
?promise.race()
Promise.race(iterable)
iterable
参数里的任意一个子 promise
被成功或失败后,父 promise
马上也会用子 promise
的成功返回值或失败详情作为参数调用父 promise
绑定的相应句柄,并返回该 promise
对象。/** * @author guoqiankunmiss */ //封装一个取消promise的函数,使用promise.race的特性 function stopPromise (stopP) { let proObj = {}; let promise = new Promise((resolve, reject) => { proObj.resolve = resolve; proObj.reject = reject; }) proObj.promise = Promise.race([stopP, promise]) return proObj } //一个5秒钟之后执行的.then方法的promise let promise = new Promise((resolve, reject) => { setTimeout(() => { resolve(123); }, 5000); }); //调用函数 let obj = stopPromise(promise); //收集返回值 obj.promise.then(res => { console.log(res); }); //两秒钟之后进行取消promise操作 setTimeout(() => { obj.resolve("Promise 请求被取消了!"); }, 2000)
promise
如何获取第一个成功promise
?Promise.all
改进利用 promise.all
的特性,遍历 promise
数组,根据返回值进行判断,当成功的时候,转为 reject
返回,当失败的时候转为 resolve
继续执行。
//第一个成功的Promise
function firstProSuccess (allProMise) {
//遍历promise数组,根据返回值进行判断,当成功的时候,转为reject返回,当失败的时候转为resolve继续执行。
return Promise.all(allProMise.map(item => {
return item.then(
res => Promise.reject(res),
err => Promise.resolve(err)
)
})).then(
errors => Promise.reject(errors),
val => Promise.resolve(val)
)
}
Promise.any
Promise.any(iterable)
Promise
对象的集合,当其中的一个 promise
成功,就返回那个成功的 promise
的值。缺点:有兼容问题
promise
,所有的 promise
都取得返回结果(不管成功/失败都要返回值)Promise.all
改进和上面原理类似,只不过是当成功的时候不进行操作,当 reject
时进行 resolve
操作
Promise.allSettled()
Promise.allSettled(iterable)
promise
都已经 fulfilled
或 rejected
后的 promise
。缺点:有兼容问题
promise
的静态方法有哪些?Promise.all(iterable)
接收一个 promise
数组对象(可迭代的 promise
实例对象),全部成功时,返回所有 promise
的数组集合;当其中一个失败时,返回当前失败的 promise
对象。
Promise.allSettled(iterable)
接收一个 promise
数组对象,全部完成时(不管成功/失败)返回新的 promise
数组集合
Promise.any(iterable)
接收一个 promise
数组对象,当其中任何一个成功时,返回成功的 promise
值
Promise.race(iterable)
接收一个 promise
数组对象,当其中任意一个成功/失败时,返回该 promise
值
Promise.reject(reason)
返回一个状态为失败的 Promise
对象。
Promise.resolve(value)
返回一个状态由给定 value
决定的 Promise
对象。
Promise.finally(onFinally)
在当前 promise
运行完毕后被调用,无论当前 promise
的状态是完成( fulfilled
)还是失败( rejected
)
Promise.try(f)
接收一个函数,返回一个 promise
。
为所有操作提供了统一的处理机制,所以如果想用 then
方法管理流程,最好都用 Promise.try
包装一下。
Promise.then
的第二个参数有了解吗?和 .catch
有什么区别?then()
方法返回一个 Promise
。
它最多需要有两个参数:Promise
的成功和失败情况的回调函数。
p.then(onFulfilled[, onRejected]);
p.then(value => {
// fulfillment
}, reason => {
// rejection
});
第二个参数也是一个函数,是对失败情况的回调函数。
then 第二个参数 | catch |
---|---|
then 方法的参数 | Promise 的实例方法 |
then 的第一个参数抛出异常捕获不到 | then 的第一个参数抛出异常可以捕获 |
是一个函数 | 本质是 then 方法的语法糖 |
如果第二个参数和 catch 同时存在,promise 内部报错,第二个参数可以捕获 | 此时,catch 捕获不到,第二个参数不存在,catch 才会捕获到 |
不建议使用 | 建议使用 catch 进行错误捕获 |
Promise.resolve
有几种情况?Promise
实例参数是 Promise
实例,那么 Promise.resolve
将不做任何修改、原封不动地返回这个实例。
thenable
对象Promise.resolve()
方法会将这个对象转为 Promise
对象,然后就立即执行 thenable
对象的 then()
方法。
then()
方法的对象,或根本就不是对象如果参数是一个原始值,或者是一个不具有 then()
方法的对象,则 Promise.resolve()
方法返回一个新的 Promise
对象,状态为 resolved
。
直接返回一个 resolved
状态的 Promise
对象。
.then
中的参数不是函数,那会怎样?Promise.resolve(1)
.then(2)
.then(console.log)
// 1
如果 .then
中的参数不是函数,则会在内部被替换为 (x) => x
,即原样返回 promise
最终结果的函数。
.finally
后面继续跟了个 .then
,那么这个 then
里面的值是什么?Promise.resolve('resolve')
.finally(() => {
console.log('this is finally')
return 'finally value'
})
.then(res => {
console.log('finally后面的then函数, res的值为:', res)
})
// this is finally
finally
后面的 then
函数, res
的值为: resolve
finally
的回调函数中不接收任何参数;promise
结束时,无论结果是 fulfilled
或者是 rejected
,都会执行 finally
回调函数;finally
返回的是一个上一次的 Promise
对象值。.all
和 .race
在传入的数组有第一个抛出异常的时候,其他异步任务还会继续执行吗?会的,会继续执行,只是不会在 then / catch
中表现出来。
浏览器执行下面代码,可以看出当报错的时候 console
还是会继续执行的,只是在 对应的回调函数里面没有表现出来。
function sleep (n) {
return new Promise((resolve, reject) => {
console.log(n)
Math.random() > 0.5 ? reject(n) : resolve(n)
}, n % 2 === 0 ? 1000 * n : 1000)
}
Promise.all([sleep(1), sleep(2), sleep(3)])
.then(res => console.log('all res: ', res))
.catch(err => console.log('all err:', err))
Promise.race([sleep(1), sleep(2), sleep(3)])
.then(res => console.log('race res: ', res))
.catch(err => console.log('race err:', err))
.all
是并发的还是串行的?是并发的,但是返回值和 promise.all
中接收到的数组顺序一样。
promise
为什么可以进行链式调用因为 then
、catch
、finally
方法会返回一个新的 promise
,所以允许我们进行链式调用。
async/await
async
函数是基于 generator
实现,所以涉及到 generator
相关知识。
在没有async
函数之前,通常使用 co
库来执行 generator
,所以通过 co
我们也能模拟 async
的实现。
co
库function Asyncfn() { return co(function*() { //..... }); } function co(gen) { return new Promise((resolve, reject) => { const fn = gen(); function next(data) { let { value, done } = fn.next(data); if (done) return resolve(value); Promise.resolve(value).then(res => { next(res); }, reject); } next(); }); }
Generator
函数和自执行器function spawn(genF) { return new Promise(function(resolve, reject) { const gen = genF(); function step(nextF) { let next; try { next = nextF(); } catch (e) { return reject(e); } if (next.done) { return resolve(next.value); } Promise.resolve(next.value).then( function(v) { step(function() { return gen.next(v); }); }, function(e) { step(function() { return gen.throw(e); }); } ); } step(function() { return gen.next(undefined); }); }); }
JSON.stringify
和 JSON.parse
JSON.stringify
定义:将一个 JavaScript
对象或值转换为 JSON
字符串。
参数:有三个参数
JSON.stringify(value[, replacer [, space]])
replacer
replacer
参数可以是一个函数或者一个数组。key
)和值( value
),它们都会被序列化。replacer
是一个数组,数组的值代表将被序列化成 JSON
字符串的属性名。space
space
参数用来控制结果字符串里面的间距。JSON.parse
定义:用来解析 JSON
字符串。
参数:有两个参数
JSON.parse(text[, reviver])
reviver
toJSON()
方法,该方法定义什么值将被序列化。undefined
、任意的函数以及 symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成 null
(出现在数组中时)。函数、undefined
被单独转换时,会返回 undefined
,如JSON.stringify(function(){})
or JSON.stringify(undefined)
。symbol
为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。Date
日期调用了 toJSON()
将其转换为了 string
字符串(同Date.toISOString()
),因此会被当做字符串处理。NaN
和 Infinity
格式的数值及 null
都会被当做 null
。Map/Set/WeakMap/WeakSet
,仅会序列化可枚举的属性。==
、===
和 Object.is()
==
两边值类型不同的时候,先进行类型转换,在比较===
不进行类型转换,直接值比较Object.is(val1, val2)
判断两个值是否为同一值==
类型转换是怎么转换的?null
或者是 undefined
,如果是,返回 true
string
或者 number
,如果是,将 string
转换为 number
boolean
,如果是,将其中一方转为 number
在进行判断object
,且另外一方是 string
、number
、symbol
,如果是,将 object
转为原始类型进行判断(valueOf()
方法)NaN
,则直接返回 false
[] == ![]
的值为什么?答案:为 true
![]
会被转换为 false
,因此此时为 [] == false
boolean
,把 boolean
转为 number
,所以此时为 [] == 0
[]
转为原始类型,调用数组的 toString()
方法,[].toString() = ''
,所以此时为 '' == 0
string
转为 number
,''
转为 number
为 0,所以此时 0 == 0
0 == 0
,为 true
不会进行强制类型转换
undefined
null
true
或 false
+0
-0
NaN
NaN
且为同一个值防抖:是多次执行改为最后一次执行
节流:是将多次执行改为每隔一段时间执行
思路:
触发高频事件后 n
秒内函数只会执行一次,如果 n
秒内高频事件再次被触发,则重新计算时间,每次触发事件时都取消之前的延时调用方法
function debounce (fn, time = 500) {
let timeout = null; // 创建一个标记用来存放定时器的返回值
return function () {
clearTimeout(timeout) // 每当触发时,把前一个 定时器 clear 掉
timeout = setTimeout(() => { // 创建一个新的 定时器,并赋值给 timeout
fn.apply(this, arguments)
}, time)
}
}
function testDebounce () {
console.log('测试防抖')
}
const inp = document.getElementById('testInp')
inp.addEventListener('input', debounce(testDebounce))
高频事件触发,但在 n
秒内只会执行一次,所以节流会稀释函数的执行频率,每次触发事件时都判断当前是否有等待执行的延时函数
function throttle (fn, time = 100) { let timeout; return function () { let context = this let args = arguments if (!timeout) { timeout = setTimeout(() => { timeout = null fn.apply(context, args) }, time) } } } function testThro () { console.log('测试节流') } const inp = document.getElementById('testInp') inp.addEventListener('input', throttle(testThro))
cookie
、sessionStorage
和localStorage
cookie
用来保存登录信息,大小限制为 4KB
左右localStorage
是 Html5
新增的,用于本地数据存储,保存的数据没有过期时间,一般浏览器大小限制在 5MB
sessionStorage
接口方法和 localStorage
类似,但保存的数据的只会在当前会话中保存下来,页面关闭后会被清空。名称 | 生命期 | 大小限制 | 与服务器通信 | 是否可以跨域 |
---|---|---|---|---|
cookie | 一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie ,默认是关闭浏览器后失效 | 4KB | 每次都会携带在 HTTP 头中,如果使用 cookie 保存过多数据会带来性能问题 | 一般不可,相同 domain 下可以允许接口请求携带 cookie |
localStorage | 除非被清除,否则永久保存 | 5MB | 仅在浏览器中保存,不与服务器通信 | 不可 |
sessionStorage | 仅在当前会话下有效,关闭页面或浏览器后被清除 | 5MB | 仅在浏览器中保存,不与服务器通信 | 不可 |
localStorage
进行怎么进行跨域存储?localStorage
是不可以进行跨域操作的,但是想进行跨域操作可以使用 postMessage
,websocket
进行变相的跨域操作。
同源策略是一个重要的安全策略,它用于限制一个 origin
的文档或者它加载的脚本如何能与另一个源的资源进行交互,它能帮助阻隔恶意文档,减少可能被攻击的媒介。
所谓同源策略,是指只有在地址的:
均一样的情况下,才允许访问相同的 cookie
、localStorage
,以及访问页面的 DOM
或是发送 Ajax
请求。
ajxa
请求Dom
的查询ajax
同源策略、Dom
同源策略)。jsonp
cors
postMessage
websocket
Node
中间件代理(两次跨域)nginx
反向代理window.name + iframe
location.hash + iframe
document.domain + iframe
CORS
常用的配置有哪些?Access-Control-Allow-Origin
允许的域名Access-Control-Allow-Methods
允许的 http
请求方法Access-Control-Allow-Headers
支持的请求头Access-Control-Allow-Credentials
是否发送 cookie
Access-Control-Max-Age
以秒为单位的缓存时间CORS
跨域的判定流程Access-Control-Allow-origin
字段进行匹配,若无该字段说明不允许跨域,报错,有该字段进行比对,判断是否可以跨域。简单请求是指满足以下条件的:
get
、post
、head
其中一种方法进行请求的;http
的头信息不超出一下情况:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
:值仅限于 application/x-www-form-urlencoded
、multipart/form-data
、text/plain
XMLHttpRequestUpload
对象没有注册任何的事件监听器;XMLHttpRequestUpload
对象可以使用 XMLHttpRequest.upload
属性访问。 请求中没有使用 ReadableStream
对象。对服务器有特殊要求的请求(简单请求之外就是非简单请求)。
例如:请求方式是 put
、delete
,Content-Type
的类型是 application/json
。
非简单请求会在正式通信前使用 options
发起一个预检请求,询问服务器当前的域名是否在服务器允许的名单之中,以及使用哪些头信息字段等。
script
标签,嵌入跨域脚本;link
标签,嵌入 css
;img
标签,嵌入图片;video/audio
标签,嵌入视频、音频;object/embed/applet
标签,嵌入 svg
/图片 等;svg
标签,嵌入 svg
;@font-face
嵌入字体;iframe
嵌入资源JSONP
//Promise封装 function jsonp({ url, params, callback }) { return new Promise((resolve, reject) => { // 创建 script 标签 let script = document.createElement('script') // 把 callback 挂载在 window 上,执行之后删除 script window[callback] = function(data) { resolve(data) document.body.removeChild(script) } // 添加参数 params = { ...params, callback } // wd=b&callback=callFun let arrs = [] for (let key in params) { arrs.push(`${key}=${params[key]}`) } // 设置 script 的 URL script.src = `${url}?${arrs.join('&')}` // 插入 body 中 document.body.appendChild(script) }) } // 调用示例 jsonp({ url: 'http://localhost:3000/code', params: { wd: 'hello world' }, callback: 'callFun' }).then(data => { console.log(data) // 你好啊 //再此回调结束后删除该script })
js
的继承方式prototype
子类型的原型为父类型的一个实例对象。
Child.prototype = new Parent()
优点:
call
在子类型构造函数中通用 call()
调用父类型构造函数
function Child(name, age, price) {
Parent.call(this, name, age) // 相当于: this.Parent(name, age)
}
优点:
prototype + call
调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Child (name, age, price) {
Parent.call(this, name, age)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child//组合继承也是需要修复构造函数指向的
优点:
通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。
function Child (name, age, price) {
Parent.call(this, name, age)
}
Child.prototype = Parent.prototype
优点:
借助原型可以基于已有的对象来创建对象,var B = Object.create(A)
以 A
对象为原型,生成了 B
对象。B
继承了 A
的所有属性和方法。
function Child (name, age, price) {
Parent.call(this, name, age)
}
Child.prototype = Object.create(Parent.prototype)
Child.prototype.constructor = Child
es6 class
的继承class
关键字只是原型的语法糖,JavaScript
继承仍然是基于原型实现的。
class Parent { constructor(name, age) { this.name = name this.age = age } setName () { console.log('parent') } } let child1 = new Parent('name1', 18) let child2 = new Parent('name2', 16) class Child extends Parent { constructor(name, age, price) { super(name, age) this.price = price } setAge () { console.log('子类方法') } } let child3 = new Child('name3', 20, 15000) let child4 = new Child('name4', 21, 10000)
优点:
简单来说就是相邻两个元素进行对比,按照你需要的排序方式(升序or降序)进行位置替换,替换时需要额外一个变量当作中间变量去暂存值。
function bubbleSort(arr) {
var len = arr.length;
for (var i = 0; i < len; i++) {
for (var j = 0; j < len - 1 - i; j++) {
if (arr[j] > arr[j+1]) { //相邻元素两两对比
var temp = arr[j+1]; //元素交换
arr[j+1] = arr[j];
arr[j] = temp;
}
}
}
return arr;
}
选择一个基准,将比基准小的放左边,比基准小的放在右边(基准处在中间位置)
function quickSort(arr) { //如果数组<=1,则直接返回 if (arr.length <= 1) { return arr; } var pivotIndex = Math.floor(arr.length / 2); //找基准,并把基准从原数组删除 var pivot = arr.splice(pivotIndex, 1)[0]; //定义左右数组 var left = []; var right = []; //比基准小的放在left,比基准大的放在right for (var i = 0; i < arr.length; i++) { if (arr[i] <= pivot) { left.push(arr[i]); } else { right.push(arr[i]); } } //递归 return quickSort(left).concat([pivot], quickSort(right)); }
首先从原始数组中找到最小的元素,并把该元素放在数组的最前面,然后再从剩下的元素中寻找最小的元素,放在之前最小元素的后面,直到排序完毕
function selectionSort(arr) { var len = arr.length; var minIndex, temp; for (var i = 0; i < len - 1; i++) { minIndex = i; for (var j = i + 1; j < len; j++) { if (arr[j] < arr[minIndex]) { minIndex = j; } } temp = arr[i]; arr[i] = arr[minIndex]; arr[minIndex] = temp; } return arr; }
从第二个元素开始(假定第一个元素已经排序了),取出这个元素,在已经排序的元素中从后向前进行比较,如果该元素大于这个元素,就将该元素移动到下一个位置,然后继续向前进行比较,直到找到小于或者等于该元素的位置,将该元素插入到这个位置后.重复这个步骤直到排序完成
function insertionSort(arr) {
var len = arr.length;
var preIndex, current;
for (var i = 1; i < len; i++) {
preIndex = i - 1;
current = arr[i];
while (preIndex >= 0 && arr[preIndex] > current) {
arr[preIndex + 1] = arr[preIndex];
preIndex--;
}
arr[preIndex + 1] = current;
}
return arr;
}
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer
)的一个非常典型的应用。归并排序是一种稳定的排序方法。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
function mergeSort(arr) { //采用自上而下的递归方法 var len = arr.length; if(len < 2) { return arr; } var middle = Math.floor(len / 2), left = arr.slice(0, middle), right = arr.slice(middle); return merge(mergeSort(left), mergeSort(right)); } function merge(left, right){ var result = []; console.time('归并排序耗时'); while (left.length && right.length) { if (left[0] <= right[0]) { result.push(left.shift()); } else { result.push(right.shift()); } } while (left.length) result.push(left.shift()); while (right.length) result.push(right.shift()); console.timeEnd('归并排序耗时'); return result; }
利用步长来进行两两元素比较,然后缩减步长在进行排序。
说明:希尔排序的实质是分组插入排序,该方法又称缩小增量排序。该方法的基本思想是:先将整个待排元素序列分割为若干个子序列(由相隔某个‘增量’的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,带这个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况)效率是很高的,因此希尔排序在时间效率上有较大的提高。
与插入排序的不同之处:它会优先比较距离较远的元素
function shellSort(arr) { let temp, gap = 1; while (gap < arr.length / 3) { gap = gap * 3 + 1//动态定义间隔序列 } for (gap; gap > 0; gap = Math.floor(gap / 3)) {//控制步长(间隔)并不断缩小 for (var i = gap; i < arr.length; i++) {//按照增量个数对序列进行排序 temp = arr[i] for (var j = i - gap; j >= 0 && arr[j] > temp; j -= gap) {//例:j=0 arr[1]>arr[5] arr[j + gap] = arr[j] } arr[j + gap] = temp } } return arr }
主要还是从算法所占用的「时间」和「空间」两个维度去考量。
「 大O符号表示法 」,即 T(n) = O(f(n))
时间复杂度的公式是: T(n) = O( f(n) )
,其中 f(n)
表示每行代码执行次数之和,而 O
表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。
• 常数阶 O(1)
• 对数阶 O(logN)
• 线性阶 O(n)
• 线性对数阶 O(nlogN)
• 平方阶 O(n²)
• 立方阶 O(n³)
• K次方阶 O(n^k)
• 指数阶 (2^n)
O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是 O(1)
var i = 1;
var j = 2;
++i;
j++;
var m = i + j;
O(n)
for
循环里面的代码会执行 n
遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用 O(n)
来表示它的时间复杂度。
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
O(logN)
var i = 1;
while(i<n)
{
i = i * 2;
}
在 while
循环里面,每次都将 i
乘以 2
,乘完之后,i
距离 n
就越来越近了。我们试着求解一下,假设循环 x
次之后,i
就大于 2
了,此时这个循环就退出了,也就是说 2
的 x
次方等于 n
,那么 x = log2^n
也就是说当循环 log2^n
次以后,这个代码就结束了。因此这个代码的时间复杂度为:O(logn)
O(nlogN)
将时间复杂度为 O(logn)
的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN)
,也就是了 O(nlogN)
。
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
O(n²)
、O(m*n)
、O(n³)
、O(n^k)
平方阶 O(n²)
把 O(n)
的代码再嵌套循环一遍,它的时间复杂度就是O(n*n)
,即 O(n²)
。
for(x=1; i<=n; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
O(m*n)
将其中一层循环的 n
改成 m
,那它的时间复杂度就变成了 O(m*n)
for(x=1; i<=m; x++)
{
for(i=1; i<=n; i++)
{
j = i;
j++;
}
}
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n)
来定义。
空间复杂度比较常用的有:O(1)、O(n)、O(n²)
。
O(1)
如果算法执行所需要的临时空间不随着某个变量 n
的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
var i = 1;
var j = 2;
++i;
j++;
var m = i + j;
代码中的 i
、j
、m
所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
O(n)
var arr = [1, 2, 3]
for(i=1; i<=arr.lemgth; ++i)
{
j = i;
j++;
}
第一行定义了一个数组出来,这个数据占用的大小为 n
,这段代码的 2-6
行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
AJAX
ajax
function stringify (json) { var str = ""; for (var i in json) { str += i + "=" + json[i] + "&"; } return str.slice(0, -1); } function myAjax (type, url, params, callback, errback) { let xhr = null; //表IE if (window.XMLHttpRequest) { xhr = new XMLHttpRequest(); } else { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } if (type == "get") { xhr.open(type, url + "?" + stringify(params), true); xhr.send(); } else { xhr.open(type, url, true); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); //json转换成name=张三&age=1 xhr.send(stringify(params)); } xhr.onreadystatechange = function () { // 表示请求已完成 if (xhr.readyState == 4) { if (xhr.status == 200) { if (callback) { callback(xhr.responseText); } } else { errback && errback(); } } } }
ajax
的 readyState
的状态0
未初始化,还没有调用 open()
方法1
启动,已经调用 open()
方法,但是没有调用 send()
方法2
发送,已经调用 send()
方法,但是尚未接收响应3
接收,已经接收到部分响应数据4
完成,已经接收到全部响应数据Axios
Axios
本质上也是对原生 XHR
的封装,只不过它是 Promise
的实现版本,符合最新的 ES
规范
node.js
创建 http
请求Promise API
CSRF
Fetch
Fetch API
提供了一个 JavaScript
接口,用于访问和操纵 HTTP
管道的部分,例如请求和响应。它还提供了一个全局 fetch()
方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
Promise
实现,支持 async/await
isomorphic-fetch
Fetch
请求默认是不带 cookie
的,需要设置 fetch(url, {credentials: 'include'})
reject
,只有网络错误这些导致请求不能完成时,fetch
才会被 reject
。new
操作符new
的实现流程this
就指向了这个新对象);prototype
关联到实例的 __proto__
new
function myNew (foo, ...args) {
// 创建一个新对象,并继承 foo 的 prototype 属性
let obj = Object.create(foo.prototype)
// 执行构造方法,并绑定新 this,
let result = foo.apply(obj, args)
// 如果构造方法返回了一个对象,那么就返回该对象,否则就返回 myNew 创建的新对象
return Object.prototype.toString().call(result) === '[object Object]' ? result : obj
}
document.documentElement.requestFullscreen()
需要兼容实现
function fullScreen() {
if (!document.fullscreenElement &&
!document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement) { // current working methods
if (document.documentElement.requestFullscreen) {
document.documentElement.requestFullscreen();
} else if (document.documentElement.msRequestFullscreen) {
document.documentElement.msRequestFullscreen();
} else if (document.documentElement.mozRequestFullScreen) {
document.documentElement.mozRequestFullScreen();
} else if (document.documentElement.webkitRequestFullscreen) {
document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
}
}
}
function exitFullScreen() {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
/**
* 检查是否全屏
* @return {[Boolean]} [是否全屏,为 true 没有全屏,false 全屏]
*/
function checkFullScreenValue () {
return !document.fullscreenElement &&
!document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement
}
Map
、WeakMap
和 set
、WeakSet
有什么区别?
WeakMap
和WeakSet
都是弱引用
弱引用是指不能确保其引用的对象不会被垃圾回收器回收的引用,换句话说就是可能在任意时间被回收。
弱引用随时都会消失,遍历机制无法保证成员的存在
Set
NaN
时,只会存在一个 NaN
5 !== "5"
)keys()
和 values()
的行为完全一致,entries()
返回的遍历器同时包括键和值且两值相等weakSet
作用Set
结构类似,成员值只能是对象DOM
节点:DOM
节点被移除时自动释放此成员,不用担心这些节点从文档移除时会引发内存泄漏WeakSet
结构中的引用就会自动消WeakSet
结构对此成员的引用ES6
规定 WeakSet
结构不可遍历WeakSet
结构中Map
NaN
作为键时,只会存在一个以 NaN
作为键的值Object
结构提供字符串—值的对应,Map
结构提供值—值的对应WeakMap
Map
结构类似,成员键只能是对象DOM
节点:DOM
节点被移除时自动释放此成员键,不用担心这些节点从文档移除时会引发内存泄漏WeakMap
结构对此成员键的引用ES6
规定 WeakMap
结构不可遍历WeakMap
结构中Object
转为 Map
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj))
Proxy
target
要使用 Proxy
包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理handler
一个通常以函数作为属性的对象,用来定制拦截行为proxy
对象生效,对代理对象没有任何效果origin = {}
obj = new Proxy(origin, {
get: function (target, propKey, receiver) {
return '10'
}
});
obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined
Handler
对象常用的方法方法 | 描述 |
---|---|
handler.has() | in 操作符的捕捉器。 |
handler.get() | 属性读取操作的捕捉器。 |
handler.set() | 属性设置操作的捕捉器。 |
handler.deleteProperty() | delete 操作符的捕捉器。 |
handler.ownKeys() | Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 |
handler.apply() | 函数调用操作的捕捉器。 |
handler.construct() | new 操作符的捕捉器 |
proxy
代理是否可以撤销?proxy
有一个唯一的静态方法,Proxy.revocable(target, handler)
Proxy.revocable()
方法可以用来创建一个可撤销的代理对象
该方法的返回值是一个对象,其结构为:{"proxy": proxy, "revoke": revoke}
proxy
表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler)
创建的代理对象没什么不同,只是它可以被撤销掉。revoke
撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。const target = { name: 'vuejs'}
const {proxy, revoke} = Proxy.revocable(target, handler)
proxy.name // 正常取值输出 vuejs
revoke() // 取值完成对proxy进行封闭,撤消代理
proxy.name // TypeError: Revoked //已撤销
一个程序中只能存在一个全局执行上下文。
这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:
window
对象。this
指针指向这个全局对象。可以有无数个函数执行上下文。
每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。
Eval
函数执行上下文:js
的 eval
函数执行其内部的代码会创建属于自己的执行上下文, 很少用而且不建议使用。
window
对象;arguments
,提升函数声明和变量声明。JavaScript
始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。this
指向:确定 this
的指向。js
如何管理多个执行上下文的?管理多个执行上下文靠的就是执行栈,也被叫做调用栈。
特点:后进先出(LIFO:last-in, first-out
)的结构。
作用:存储在代码执行期间的所有执行上下文。
示例:
var a = 1; // 1. 全局上下文环境
function bar (x) {
console.log('bar')
var b = 2;
fn(x + b); // 3. fn上下文环境
}
function fn (c) {
console.log(c);
}
bar(3); // 2. bar上下文环境
function once (func) {
let done;
return function () {
if (!done) {
func.apply(null, arguments)
done = true
}
}
}
const onlyDoOne = once(function() {
console.log('1')
})
onlyDoOne() // 1
onlyDoOne() // 没有输出,不会再次执行
function sleep (time) {
return new Promise(resolve => {
window.setTimeout(resolve, time)
})
}
// 调用
sleep(1000).then(res => {
console.log('延迟')
})
// 调用
async function useSleep () {
const sleepval = await sleep(1000)
}
useSleep()
setTimeout
实现 setInterval
;(() => { const list = new Set(); function myInterval(fn, ms) { const ref = {}; const exec = () => { return setTimeout(() => { fn.apply(null); const timer = exec(); ref.current = timer; }, ms); }; ref.current = exec(); list.add(ref); return ref; } function myClearInterval(ref) { clearTimeout(ref.current); list.delete(ref); } window.myInterval = myInterval; window.myClearInterval = myClearInterval; })() myInterval(() => {console.log(1)}, 5000) myClearInterval({current: 1186})
excel
表格并下载/** * 前端下载表格 * @param {[Array]} data [数据数组] * @param {[String]} tableHead [表头字符串] * @return {[undefined]} */ function downExcel (data, tableHead) { tableHead = tableHead data.forEach(item => { for (let i in item) { tableHead += `${item[i] + '\t'},` } tableHead += '\n' }) const url = 'data:text/csv;charset=utf-8,\ufeff' + encodeURIComponent(tableHead); //通过创建a标签实现 const link = document.createElement("a"); link.href = url; //对下载的文件命名 link.download = "我的EXCEL表格.csv"; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // excel 数据 let tableData = [{ name: '你好啊', time: 130000000000, pre: '127.130', source: '淘宝', otherTime: 1571276232000 }] // excel 头部 let str = `用户名,时间,坐标,来源,授权时间\n`; // 下载表格执行 downExcel(tableData, str)
原型:JS
声明构造函数(用来实例化对象的函数)时,会在内存中创建一个对应的对象,这个对象就是原函数的原型。
构造函数默认有一个 prototype
属性,prototype
的值指向函数的原型。同时原型中也有一个 constructor
属性,constructor
的值指向原函数。
通过构造函数实例化出来的对象,并不具有 prototype
属性,其默认有一个 __proto__
属性,__proto__
的值指向构造函数的原型对象。在原型对象上添加或修改的属性,在所有实例化出的对象上都可共享。
当在实例化的对象中访问一个属性时,首先会在该对象内部寻找,如找不到,则会向其 __proto__
指向的原型中寻找,如仍找不到,则继续向原型中 __proto__
指向的上级原型中寻找,直至找到或 Object.prototype
为止,这种链状过程即为原型链。
EventBus
简单实现
class myEventBus { constructor(props) { this.events = {} } on (event, fn) { const events = this.events events[event] ? events[event].push(fn) : (events[event] = [fn]) } emit (event, ...res) { this.events[event] && this.events[event].forEach(fn => { return fn.apply(this, res) }) } remove (event, fn) { if (this.events[event]) { delete this.events[event] } } }
js
的垃圾回收(GC
)V8
内存限制V8
内存管理JS
对象都是通过 V8
进行分配管理内存的process.memoryUsage()
返回一个对象,包含了 Node
进程的内存占用信息var a = {name:‘yuhua’};
这句代码会做如下几步:
Code Segment
”a
放入“栈( Stack
):本地变量、指针”{name:‘yuhua’}
放入“ HeapTotal
(堆):对象,闭包”V8
垃圾收集工作原理导致的,1.4G 内存完全一次垃圾收集需要 1s 以上Stop The World
,在这期间,应用的性能和响应能力都会下降V8
的垃圾回收机制V8
是基于分代的垃圾回收From
区域和 To
区域两个区域组成From
区域和 To
区域各占 16MFrom
区域和 To
区域各占 8MScavenge
算法Scavenge
为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。
新生代扫描的时候是一种广度优先的扫描策略
它将内存分为 from
和 to
两个空间。每次 gc
,会将 from
空间的存活对象复制到 to
空间。然后两个空间角色对换(又称反转)。
该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。
FROM
区域中的存活对象,如果还活着,拷贝到 TO
空间,所有存活对象拷贝完后,清空(释放) FROM
区域TO
的空间使用占比超过 25%,或者超大对象memory
中可以通过拍快照看变量是否被垃圾回收undefined
或 null
都能将引用计数减去 1Mark-Sweep
和 Mark-Compact
mark-sweep
标记清除
mark-compact
标记整理
GC
耗时比较长GC
期间无法想听,STOP-THE-WORLD
V8
有一个优化方案,增量处理,把一个大暂停换成多个小暂停 INCREMENT-GC
假设有10个大小的内存,内存占用了6个,
Mark-Sweep
模式垃圾回收:A b C d E f 空 空 空 空
//对上面每个对象做上标记,大写表示活着,小写表示死了
//这时候,会存在一个问题,就是内存碎片无法使用,因为小写的内存没有跟后面空空空空的内存放在一起,不能使用
Mark-Compact
模式垃圾回收A C E b d f 空 空 空 空
A C E 空 空 空 空 空 空 空
回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少 | 少 | 双倍空间(无碎片) |
是否移动对象 | 否 | 是 | 是 |
V8
老生代主要用 Mark-Sweep
,因为 Mark-Compact
需要移动对象,执行速度不快。空间不够时,才会用 Mark-Compact
SRP
)一个对象或方法只做一件事情。
LKP
)应当尽量减少对象之间的交互。
OCP
)软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改
策略模式是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。
优点:
示例:
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果,提供效率以及节省开销。
缓存代理,就是将前面使用的值缓存下来,后续还有使用的话,就直接拿出来用。
工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。
简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
工厂方法模式的本意是将实际创建对象的工作推迟到子类中,工厂方法模式就是将这个大厂拆分出各个小厂,每次添加新的产品让小厂去生产,大厂负责指挥就好了。
抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
确保了只有一个实例
前端应用场景:
window
对象。在 JavaScript
开发中,对于这种只需要一个的对象,往往使用单例实现。为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式主要有三种:保护代理、虚拟代理、缓存代理
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
JS
中数组的 map
forEach
已经内置了迭代器
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知。
JS中的事件就是经典的发布-订阅模式的实现
用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系
命令(command
)指的是一个执行某些特定事情的指令
strict
模式下会报错,非 strict
模式下静默失败。IIFE
中的函数是函数表达式,而不是函数声明。FD
)Program level
),要么处于其它函数的主体(FunctionBody
)中function funName () {}
FE
)// 函数表达式 var foo = function () {} // 匿名函数表达式赋值给变量foo var foo2 = function _foo2() {} // 外部FE通过变量“foo”来访问——foo(),而在函数内部(如递归调用),有可能使用名称“_foo”。 // 圆括号(分组操作符)内只能是表达式 (function foo() {}); // 在数组初始化器内只能是表达式 [function bar() {}]; // 逗号也只能操作表达式 1, function baz() {}; // ! !function() {}(); (function foo() {})() // 自执行函数 IIFE (function () {})() // IIFT var foo = { bar: function (x) { return x % 2 != 0 ? 'yes' : 'no'; }(1) }; foo.bar // 'yes'
我们将它与 FD
和 FE
区分开来。其主要特点在于这种函数的[[Scope
]]属性仅包含全局对象
var x = 10;
function foo() {
var x = 20;
var y = 30;
var bar = new Function('alert(x); alert(y);');
bar(); // 10, "y" 未定义
}
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
foo.bar // 'yes'
()
有些可以不加?当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成 FE
。
如果解析器知道它处理的是 FE
,就没必要用圆括号。
当函数表达式 FE
有一个名称(称为命名函数表达式,缩写为 NFE
)时,将会出现一个重要的特点。
从定义(正如我们从上面示例中看到的那样)中我们知道函数表达式不会影响一个上下文的变量对象(那样意味着既不可能通过名称在函数声明之前调用它,也不可能在声明之后调用它)。
但是,FE
在递归调用中可以通过名称调用自身。
(function foo(bar) {
if (bar) {
return;
}
foo(true); // "foo" 是可用的
})();
“foo
” 储存在什么地方?在 foo
的活动对象中?不是,因为在 foo
中没有定义任何” foo
”。在上下文的父变量对象中创建 foo
?也不是,因为按照定义—— FE
不会影响 VO
(变量对象)——从外部调用 foo
我们可以实实在在的看到。那么在哪里呢?
当解释器在代码执行阶段遇到命名的 FE
时,在 FE
创建之前,它创建了辅助的特定对象,并添加到当前作用域链的最前端。然后它创建了 FE
,此时(正如我们在第四章 作用域链知道的那样)函数获取了[[Scope
]] 属性——创建这个函数上下文的作用域链)。此后,FE
的名称添加到特定对象上作为唯一的属性;这个属性的值是引用到 FE
上。最后一步是从父作用域链中移除那个特定的对象。
// 例一 +function foo(){ foo=10;//我的问题代码 console.log(foo);//方法自己 }(); console.log(typeof foo);//undefined 观察是否全局污染 // 例二 var b = 10; (function b() { // 内部作用域,会先去查找是有已有变量b的声明,有就直接赋值20,确实有了呀。发现了具名函数 function b(){},拿此b做赋值; // IIFE的函数无法进行赋值(内部机制,类似const定义的常量),所以无效。 // (这里说的“内部机制”,想搞清楚,需要去查阅一些资料,弄明白IIFE在JS引擎的工作方式,堆栈存储IIFE的方式等) b = 20; console.log(b); // [Function b] console.log(window.b); // 10,不是20 })(); // 严格模式 会报错 var b = 10; (function b() { 'use strict' b = 20; console.log(b) })() // "Uncaught TypeError: Assignment to constant variable." // 普通函数 function a () { a = 1 console.log(a) } a() // 1 a() // a is not a function
XSS
攻击和 CSRF
攻击XSS
攻击XSS(Cross Site Scripting)
:跨域脚本攻击。
不需要你做任何的登录认证,它会通过合法的操作(比如在 url
中输入、在评论框中输入),向你的页面注入脚本(可能是 js
、hmtl
代码块等)。
script
、style
、iframe
等节点)DOM Parse
转换,校正不配对的 DOM
标签。HttpOnly
。DOM
:恶意修改 DOM
结构,基于客户端CSRF
攻击SRF(Cross-site request forgery)
:跨站请求伪造。
A
,并在本地生成 Cookie
。(如果用户没有登录网站 A
,那么网站 B
在诱导的时候,请求网站 A
的 api
接口时,会提示你登录)。A
的情况下,访问危险网站 B
(其实是利用了网站 A
的漏洞)。token
验证;token
隐藏在 http
请求的 head
中。referer
验证;验证页面来源。CSRF
:需要用户先登录网站 A
,获取 cookie
。XSS
:不需要登录。CSRF
:是利用网站 A
本身的漏洞,去请求网站 A
的 api
。XSS
:是向网站 A
注入 JS
代码,然后执行 JS
里的代码,篡改网站 A
的内容。input
输入框输入即请求后端接口,频繁请求之后怎样确定最后一次接口的返回值?前端请求接口的时候会把 input 输入框中的值传给后端,此时后端返回接口数据时把前端传入的值返回回去,页面渲染时只需要进行判断即可。
当再次请求的时候把上次的请求终止掉:
ajax
:abort()
axios
: CancelToken
fetch
:AbortController
百度用的就是这种取消请求的方式
js:https://ss1.bdstatic.com/5eN1bjq8AAUYm2zgoY3K/r/www/cache/static/protocol/https/amd_modules/@baidu/search-sug_54d848a.js
ID
,接口请求之前自增,然后请求接口闭包保存此值,返回之后进行两者判断。此种方式就是不用后端返回值,前端存储对应的值信息,进行判断处理
let id = 1 function ajax() { ++id console.log(id) function getData () { const newId = id const time = Math.random() * 5000 | 0 // 定义一个随机值 console.log('time', time) setTimeout(() => { console.log('id newId', id, newId) if (id === newId) { // 在此进行数据处理 console.log('this is true-->', id) } }, time) } getData() } // click 频繁点击出发函数 document.getElementById('ajaxbtn').onclick = function () { ajax() }
rem
rem(font size of the root element)
是指相对于根元素的字体大小的单位。
1rem
等于根元素 htm
的 font-size
,即只需要设置根元素的 font-size
,其它元素使用 rem
单位时,设置成相应的百分比即可。
rem(倍数) = width / (html的font-size)=> width = (html的font-size) * rem(倍数)
只要 html
的 font-size
的大小变了,width
就会自动变,所以 rem
是通过动态设置 html
的 font-size
来改变 width
的大小,以达到网页自适应大小的目的
定义公式:rem(倍数) = width / (html的font-size),根据公式我们可以得出:
rem(倍数) = 设计稿宽度( imgWidth ) / 你设置的font-size( defalutSize )
rem(倍数) = 网页的实际宽度(screenWidth) / 你需要动态设置的font-size( x ) ,那么得出设置html的font-size的公式为:
<script type="text/javascript">
(function(w,d) {
function setSize() {
var screenWidth = d.documentElement.clientWidth;
var currentFontSize = screenWidth * 100 / 750;
d.documentElement.style.fontSize = currentFontSize + 'px';
}
w.addEventListener('resize',setSize);
w.addEventListener('pageShow',setSize)
w.addEventListener('DOMContentLoaded',setSize)
})(window,document)
</script>
function setHtmlSize(){ var pageWidth = window.innerWidth; if(typeof pageWidth != "number"){ if(document.compatMode == "number"){ pageWidth = document.documentElement.clientWidth; }else{ pageWidth = document.body.clientWidth; } } var fontSize = (window.innerWidth * 100) / 750; if(fontSize<40){ fontSize = 40; } //根据屏幕大小确定根节点字号 document.getElementsByTagName('html')[0].style.fontSize = fontSize + 'px'; } function resize(){ setHtmlSize(); } if (window.attachEvent) { window.attachEvent("resize", resize); } else if (window.addEventListener) { window.addEventListener("resize", resize, false); } setHtmlSize();
750
宽度来算,1rem = 100px
,iphone6/7/8 plus
中设置 width: 6.5rem
元素的宽为多少?plus
中宽度为 414
所以宽度为 414 / 750 * 6.5 * 100
0.32 rem
为
414 / 750 * 0.32 * 100
dns-prefetch
、prefetch
、preload
、defer
、async
dns-prefetch
域名转化为 ip
是一个比较耗时的过程,dns-prefetch
能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要 dns
预取。
//来自百度首页
<link rel="dns-prefetch" href="//m.baidu.com">
prefetch
prefetch
一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载 prefetch
的资源。
<link rel="prefetch" href="http://www.example.com/">
preload
和 prefetch
不同,prefecth
通常是加载接下来可能用到的页面资源,而 preload
是加载当前页面要用的脚本、样式、字体、图片等资源。所以 preload
不是空闲时加载,它的优先级更强,并且会占用 http
请求数量。
<link rel='preload' href='style.css' as="style" οnlοad="console.log('style loaded')"
as
值包括script
style
image
media
document
onload
方法是资源加载完成的回调函数defer
和 async
//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>
defer
和 async
都是异步(并行)加载资源,不同点是 async
是加载完立即执行,而 defer
是加载完不执行,等到所有元素解析完再执行,也就是 DOMContentLoaded
事件触发之前。
因为 async
加载的资源是加载完执行,所以它比不能保证顺序,而 defer
会按顺序执行脚本。
HTML
,生成 DOM
树CSS
,生成 CSSOM
树DOM
树和 CSSOM
树结合,生成渲染树(Render Tree
)Layout
(回流):根据生成的渲染树,进行回流(Layout
),得到节点的几何信息(位置,大小)Painting
(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素Display
:将像素发送给 GPU
,展示在页面上。(这一步其实还有很多内容,比如会在 GPU
将多个合成层合并为同一个层,并展示在页面中。而 css3
硬件加速的原理则是新建合成层)DOM
元素color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。css
table
布局。DOM
树的最末端改变 class
。position
属性为 absolute
或 fixed
的元素上CSS
表达式(例如:calc()
)CSS3
硬件加速(GPU
加速)
transform
opacity
filters
Will-change
JavaScript
style
属性,或者将样式列表定义为 class
并一次性更改 class
属性,修改 style
的 cssText
属性或者修改元素的 className
值。DOM
,创建一个 documentFragment
,在它上面应用所有 DOM
操作,最后再把它添加到文档中display: none
,操作结束后再把它显示出来。因为在 display
属性为 none
的元素上进行的 DOM
操作不会引发回流和重绘css3
硬件加速,可以让 transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如 background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。浏览器接收到页面文档后,会将文档中的标记语言解析为 DOM
树。DOM
树和 CSS
结合后形成浏览器构建页面的渲染树。
渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU
形成渲染纹理,而图层在 GPU
中 transform
是不会触发 repaint
的,最终这些使用 transform
的图层都会由独立的合成器进程进行处理。
3D
或者 CSS transform
<video>
和 <canvas>
标签CSS filters
z-index
属性3D
和 2D transform
的区别就在于,浏览器在页面渲染前为 3D
动画创建独立的复合图层,而在运行期间为 2D
动画创建。动画开始时,生成新的复合图层并加载为 GPU
的纹理用于初始化 repaint
。然后由 GPU
的复合器操纵整个动画的执行。最后当动画结束时,再次执行 repaint
操作删除复合图层。
GPU
加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。GPU
渲染会影响字体的抗锯齿效果。这是因为 GPU
和 CPU
具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。JSBridge
JSBridge
JSBridge
是一种 JS
实现的 Bridge
,连接着桥两端的 Native
和 H5
。它在 APP
内方便地让 Native
调用 JS
,JS
调用 Native
,是双向通信的通道。JSBridge
主要提供了 JS
调用 Native
代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。
H5
和 native
的区别name | H5 | Native |
---|---|---|
稳定性 | 调用系统浏览器内核,稳定性较差 | 使用原生内核,更加稳定 |
灵活性 | 版本迭代快,上线灵活 | 迭代慢,需要应用商店审核,上线速度受限制 |
受网速 影响 | 较大 | 较小 |
流畅度 | 有时加载慢,给用户“卡顿”的感觉 | 加载速度快,更加流畅 |
用户体验 | 功能受浏览器限制,体验有时较差 | 原生系统 api 丰富,能实现的功能较多,体验较好 |
可移植性 | 兼容跨平台跨系统,如 PC 与 移动端,iOS 与 Android | 可移植性较低,对于 iOS 和 Android 需要维护两套代码 |
JSBridge
的用途JSBridge
就像其名称中的『Bridge
』的意义一样,是 Native
和非 Native
之间的桥梁,它的核心是 构建 Native
和非 Native
间消息通信的通道,而且是 双向通信的通道。
双向通信的通道:
JS
向 Native
发送消息 : 调用相关功能、通知 Native
当前 JS
的相关状态等。Native
向 JS
发送消息 : 回溯调用结果、消息推送、通知 JS
当前 Native
的状态等。JSBridge
流程
H5
->通过某种方式触发一个url
->Native
捕获到url
,进行分析->原生做处理->Native
调用H5
的JSBridge
对象传递回调。
实现流程
Native
与 JS
交互的全局桥对象JS
如何调用 Native
Native
如何得知 api
被调用url-
参数和回调的格式Native
如何调用 JS
H5
中 api
方法的注册以及格式JSBridge
的实现原理JavaScript
调用 Native
推荐使用 注入 API
的方式(iOS6
忽略,Android 4.2
以下使用 WebViewClient
的 onJsPrompt
方式)。Native
调用 JavaScript
则直接执行拼接好的 JavaScript
代码即可。React Native
的 iOS
端举例:JavaScript
运行在 JSCore
中,实际上可以与上面的方式一样,利用注入 API
来实现 JavaScript
调用 Native
功能。不过 React Native
并没有设计成 JavaScript
直接调用 Object-C
,而是 为了与 Native
开发里事件响应机制一致,设计成 需要在 Object-C
去调 JavaScript
时才通过返回值触发调用。原理基本一样,只是实现方式不同。
Native
调 JS
native
调用 js
比较简单,只要遵循:”javascript:
方法名(‘参数,需要转为字符串’)”的规则即可。
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override public void onReceiveValue(String value) { //这里的value即为对应JS方法的返回值 }
});
IOS
Native
通过 stringByEvaluatingJavaScriptFromString
调用 Html
绑定在 window
上的函数。
JS
调 Native
Native
中通过 addJavascriptInterface
添加暴露出来的 JS
桥对象,然后再该对象内部声明对应的 API
方法。
private Object getJSBridge(){
Object insertObj = new Object(){ @JavascriptInterface public String foo(){ return "foo";
} @JavascriptInterface public String foo2(final String param){ return "foo2:" + param;
}
}; return insertObj;
}
IOS
Native
中通过引入官方提供的 JavaScriptCore
库(iOS7
以上),然后可以将 api
绑定到 JSContext
上(然后 Html
中 JS
默认通过 window.top.*
可调用)。
JSBridge
接口实现JSBridge
的接口主要功能有两个:
调用 Native
(给 Native
发消息) 和 接被 Native
调用(接收 Native
消息)。
Native
功能时 Callback
怎么实现的?JSBridge
的 Callback
,其实就是 RPC
框架的回调机制。当然也可以用更简单的 JSONP
机制解释:
当发送
JSONP
请求时,url
参数里会有callback
参数,其值是 当前页面唯一 的,而同时以此参数值为key
将回调函数存到window
上,随后,服务器返回script
中,也会以此参数值作为句柄,调用相应的回调函数。
callback
参数这个 唯一标识 是这个回调逻辑的关键。这样,我们可以参照这个逻辑来实现 JSBridge
:用一个自增的唯一 id
,来标识并存储回调函数,并把此 id
以参数形式传递给 Native
,而 Native
也以此 id
作为回溯的标识。这样,即可实现 Callback
回调逻辑。
(function () { var id = 0, callbacks = {}, registerFuncs = {}; window.JSBridge = { // 调用 Native invoke: function(bridgeName, callback, data) { // 判断环境,获取不同的 nativeBridge var thisId = id ++; // 获取唯一 id callbacks[thisId] = callback; // 存储 Callback nativeBridge.postMessage({ bridgeName: bridgeName, data: data || {}, callbackId: thisId // 传到 Native 端 }); }, receiveMessage: function(msg) { var bridgeName = msg.bridgeName, data = msg.data || {}, callbackId = msg.callbackId, // Native 将 callbackId 原封不动传回 responstId = msg.responstId; // 具体逻辑 // bridgeName 和 callbackId 不会同时存在 if (callbackId) { if (callbacks[callbackId]) { // 找到相应句柄 callbacks[callbackId](msg.data); // 执行调用 } } elseif (bridgeName) { if (registerFuncs[bridgeName]) { // 通过 bridgeName 找到句柄 var ret = {}, flag = false; registerFuncs[bridgeName].forEach(function(callback) => { callback(data, function(r) { flag = true; ret = Object.assign(ret, r); }); }); if (flag) { nativeBridge.postMessage({ // 回调 Native responstId: responstId, ret: ret }); } } } }, register: function(bridgeName, callback) { if (!registerFuncs[bridgeName]) { registerFuncs[bridgeName] = []; } registerFuncs[bridgeName].push(callback); // 存储回调 } }; })();
JSBridge
如何引用Native
端进行注入注入方式和 Native
调用 JavaScript
类似,直接执行桥的全部代码。
优点:桥的版本很容易与 Native
保持一致,Native 端不用对不同版本的 JSBridge
进行兼容;与此同时,
缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript
端在调用接口时,需要优先。
JavaScript
端引用直接与 JavaScript
一起执行。
优点:JavaScript
端可以确定 JSBridge
的存在,直接调用即可;
缺点:如果桥的实现方式有更改,JSBridge
需要兼容多版本的 Native Bridge
或者 Native Bridge
兼容多版本的 JSBridge
。
web worker
web worker
?有哪些好处?有哪些问题?Web Worker
就是为 JavaScript
创造多线程环境,允许主线程创建 Worker
线程,将一些任务分配给后者运行。在主线程运行的同时,Worker
线程在后台运行,两者互不干扰。等到 Worker
线程完成计算任务,再把结果返回给主线程。
好处就是,一些计算密集型或高延迟的任务,被 Worker
线程负担了,主线程(通常负责 UI
交互)就会很流畅,不会被阻塞或拖慢。
Worker
线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker
比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
web worker
有哪些限制?分配给 worker
的脚本文件,必须与主线程脚本文件同源。
DOM
限制worker
线程无法读取主线程所在网页的 DOM
对象,无法使用 document
、window
、parent
这些对象,可以使用 navigator
和 location
对象。
worker
线程和主线程不再同一个上下文环境中,不能直接通信,必须通过消息完成。
worker
线程不能执行 alert
方法和 confirm
方法,但是可以发出 ajax
请求。
worker
线程无法读取本地文件,不能打开文件系统,所加载的脚本,必须来自网络,不能是 file://
文件。
worker
线程怎样监听主线程的消息的?如何发送消息的?worker
线程又是如何关闭的?Worker
线程内部需要有一个监听函数,监听 message
事件。
// 监听
self.addEventListener('message', function (e) {
// 发送消息
self.postMessage('You said: ' + e.data);
}, false);
worker
线程worker
线程worker.terminate()
worker
线程关闭self.close()
worker
线程如何加载其他脚本?importScript('scripts.js')
importScript('scripts1.js', 'scripts2.js')
worker
线程的 API
主线程 | worker 线程 |
---|---|
Worker.onerror :指定 error 事件的监听函数 | self.name : Worker 的名字 |
Worker.onmessage :指定 message 事件的监听函数 | self.onmessage :指定 message 事件的监听函数 |
Worker.onmessageerror :指定 messageerror 事件的监听函数 | self.onmessageerror :指定 messageerror 事件的监听函数 |
Worker.postMessage() :向 Worker 线程发送消息 | self.close() :关闭 Worker 线程 |
Worker.terminate() :立即终止 Worker 线程 | self.postMessage() :向产生这个 Worker 线程发送消息 |
self.importScripts() :加载 JS 脚本 |
webSocket
webSocket
?有什么特点?ws
客户端与服务端数据交换时,数据包头部较小,更好的控制开销;TCP
协议之上,服务器端的实现比较容易。HTTP
协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用 HTTP
协议,因此握手时不容易屏蔽,能通过各种 HTTP
代理服务器。ws
(如果加密,则为 wss
),服务器网址就是 URL
。webSocket
的链接状态?0 (WebSocket.CONNECTING)
正在链接中1 (WebSocket.OPEN)
已经链接并且可以通讯2 (WebSocket.CLOSING)
连接正在关闭3 (WebSocket.CLOSED)
连接已关闭或者没有链接成功Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。