一、什么是编译型语言?什么是解释型语言?JS
是什么类型的语言?
1、编译型语言
1. 解释
程序在执行之前需要一个专门的编译过程,把程序编译成 为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。
2. 优缺点
程序执行效率高,依赖编译器,跨平台性差些。
3. 举例
C
、C++
都是编译型语言。
2、解释型语言
1. 解释
程序不需要编译,程序在运行时才翻译成机器语言,每执 行一次都要翻译一次。
2. 优缺点
解释型语言执行效率较低,且不能脱离解释器运行,但它的跨平台型比较容易,只需提供特定解释器即可。
3. 举例
python
、JS
都是解释型语言。
二、强制类型转换 & 隐式类型转换
1、JS
中有哪些强制类型转换和隐式类型转换?
1. 强制类型转换
String()
Number()
Boolean()
parseInt()
parseFloat()
2. 隐式类型转换
+ string
转为数字a + " "
转为字符串!var
转为布尔值
三、基本数据类型和引用数据类型
1、区别
1. 作为函数的参数时:
- 基本数据类型传入的是数据的副本,原数据的更改不会影响传入后的数据。
- 引用数据类型传入的是数据的引用地址,原数据的更改会影响传入后的数据。
2. 内存中的存储位置:
- 基本数据类型存储在栈中。
- 引用数据类型在栈中存储了指针,该指针指向的数据实体存储在堆中。
2、栈和堆在内存中的分配
eg
:var a = {name: 'yuhua'}
变量存储情况
- 将这句代码放入
代码区域 Code Segment
; - 将变量
a
放入栈(Stack):本地变量、指针
; - 将
{name: 'yuhua'}
放入HeapTotal(堆):对象,闭包
。
3、symbol
1. symbol
作为一个对象的键名时,如何获取?
不能获取 symbol
键:
for in
与for of
循环遍历中,不会获取symbol
键;Object.keys()
、Object.getOwnPropertyNames()
、JSON.stringify()
方法获取不到symbol
键;
能获取 symbol
键:
Object.getOwnPropertySymbols()
方法可以获取,返回一个数组;Reflect.ownKeys()
可以获取所有的键名,包括symbol
键
2. symbol
的类型转换
1)可以转换为 string
const symbolKey = Symbol(123)
String(symbolKey) // "Symbol(123)"
symbolKey.toString() // "Symbol(123)"
2)可以转化为布尔值
Boolean(symbolKey) // true
3)不能转化为数字(报错)
Number(symbolKey)
Uncaught TypeError: Cannot convert a Symbol value to a number
at Number (<anonymous>)
at <anonymous>:1:1
4)转化为对象
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]"
4、字符串转函数
1. eval()
let funcStr = "function test(value){alert(value)}";
let test = eval("(false || "+funcStr+")");
test("函数能够执行");
2. 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
的区别
1、Null
null
表示一个"无"的对象,转为数值为0
;- 作为函数的参数,表示该函数的参数不是对象;
- 作为对象原型链的终点。
Number(null)
为0
5 + null
位5
2、Undefined
- 变量被声明了,但是没有赋值,就等于
undefined
; - 调用函数时,应该提供的参数没有提供,该参数等于
undefined
; - 对象没有赋值属性,该属性的值为
undefined
; - 函数没有返回值时,默认返回
undefined
; Number(undefined)
为NaN
;5 + undefined
为NaN
。
五、typeof
和 instanceof
的区别
1、主要区别
typeof
表示对某个变量类型的检测,基本数据类型除了null
都能正常的显示为对应的类型,引用类型除了函数会显示为function
外,其他的都是会显示为object
;instanceof
用于检测某个构造函数的原型对象在不在某个对象的原型链上。
2、typeof
对 null
的错误显示
这只是 JS 存在的一个悠久 Bug。在 JS 的最初版本中使用的是 32 位系统,为了性能考虑使用低位存储变量的类型信息,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object 。
3、实现一个 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
1、描述下 this
对于函数而言指向最后调用函数的那个对象,是函数运行时内部自动生成的一个内部对象,只能在函数内部使用;对于全局来说,this
指向 window
。
2、函数内的 this
是在什么时候确定的?
函数调用时,指向最后调用的那个对象
3、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
4、this 的指向有哪几种?
- 默认绑定:全局环境中,
this
默认绑定到window
。 - 隐式绑定:一般地,被直接对象所包含的函数调用时,也称为方法调用,
this
隐式绑定到该直接对象。 - 隐式丢失:隐式丢失是指被隐式绑定的函数丢失绑定对象,从而默认绑定到
window
。显式绑定:通过call()
、apply()
、bind()
方法把对象绑定到this
上,叫做显式绑定。 new
绑定:如果函数或者方法调用之前带有关键字new
,它就构成构造函数调用。对于this
绑定来说,称为new
绑定。
5、箭头函数的 this
- 箭头函数没有
this
,所以需要通过查找作用域链来确定this
的值,这就意味着如果箭头函数被非箭头函数包含,this
绑定的就是最近一层非箭头函数的this
。 - 箭头函数没有自己的
arguments
对象,但是可以访问外围函数的arguments
对象。 - 不能通过
new
关键字调用,同样也没有new.target
值和原型。
6、手动实现 call
、apply
和 bind
?
1. 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 // 返回函数执行结果
}
2. 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 // 返回结果
}
3. 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
}
7、判断 this
指向
1. 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
2. testcopy()
var a = 1
function test () {
console.log(this.a)
}
const obj = {
a: 2,
test
}
const testCopy = obj.test
testCopy() // 1
// this 指向是在函数执行时确定
3. 在 setTimeout
中
var a = 1
function test () {
console.log(this.a)
}
const obj = {
a: 2,
test
}
setTimeout(obj.test) // 1
// this 指向是在函数执行时确定
七、JS
模块化
1、模块化发展历程
IIFE
自执行函数AMD
使用requireJS
来编写模块化(依赖必须提前声明好。)CMD
使用seaJS
来编写模块化(支持动态引入依赖文件。)CommonJS
nodeJs
中自带的模块化UMD
兼容AMD
、CommonJS
语法webpack(require.ensure)
:webpack 2.x
版本中的代码分割ES Modules
:ES6
引入的模块化,支持import
来引入另一个js
script
标签type="module"
2、AMD
和 CMD
的区别
AMD
和CMD
最大的区别是对依赖模块的执行时机处理不同,注意不是加载的时机或者方式不同,二者皆为异步加载模块
AMD
推崇依赖前置,在定义模块的时候就要声明其依赖的模块CMD
推崇就近依赖,只有在用到某个模块的时候再去require
3、CommonJS
规范的特点
- 所以代码都是运行在模块作用域中,不会污染全局作用域
- 模块是同步加载的,只有引入的模块加载完成,才会执行后面的操作
- 模块在首次执行后就会缓存,再次加载只返回缓存的结果
CommonJS
输出的是值的拷贝,模块内部再次改变也不会影响这个值(引用类型和基本类型有区别)
4、ES6 modules
规范有什么特点
- 输出使用
export
- 引入使用
import
- 可以使用
export ... from ...
来达到一个中转的效果 - 输入的模块变量是不可重新赋值的。只是个可读引用,但是可以改写属性
export
和import
命令处于模块顶层,不能位于作用域内,处于代码块中,没法做静态优化,违背了ES6
模块的设计初衷import
有提升效果,会提升到整个模块的头部,首先执行Babel
会把export/import
转化为exports/require
的形式,所以可以使用exports
和import
5、CommonJS
和 ES6 Modules
规范的区别
CommonJS
模块是运行时加载,ES6Modules
是编译时加载CommonJS
输出值的拷贝,ES6Modules
输出值的引用(模块内部改变会影响引用)CommonJS
导入模块可以是一个表达式(是使用require()
引入),ES6Modules
导入只能是字符串CommonJS
中 this 指向当前模块,ES6Modules
中this
指向undefined
ES6Modules
中没有arguments
、require
、module
、exports
、__filename
、__dirname
这些顶层变量
6、如何异步进行模块的加载
AMD
和 CMD
支持异步加载模块
7、开发一个模块需要考虑哪些问题?
- 安全性
- 封闭性
- 避免变量冲突
- 隔离作用域
- 公共代码的抽离
8、node require(X)
引入的处理顺序是什么样的?
- 如果
X
是内置模块,返回该模块,不再继续执行; - 如果
X
以'./'、'/'、'../'
开头,将根据X
所在的父模块,确定X
的绝对路径: a. 将X
当成文件,依次查找,存在,返回该文件,不再继续执行; b. 将X
当成目录,依次查找目录下的文件,存在,返回该文件,不再继续执行; - 如果
X
不带有路径: a. 根据X
所在的父模块,确定X
可能的安装目录 b. 依次在每个目录中,将X
当成文件名或者目录名加载 - 抛出
not found
错误
9、node 中相互引用
有个 a.js
和 b.js
两个文件,互相引用
1. 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
的执行结果,即它的第四行。
2. 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
事件
1、什么是事件委托
事件委托/事件代理:一般来说就是通过事件冒泡把一个元素的响应事件的函数代理到它的父层或者更外层元素上。
缺点:
- 只能支持冒泡的事件,对于不冒泡的事件无法代理(
focus/blur
) - 所有事件都代理容易出错,建议就近委托
- 内部元素层级过多,容易被某层阻止掉
2、document
、window
、html
、body
的层级关系
window > document > html > body
window
是BOM
的核心对象,一方面用来获取或者设置浏览器的属性和行为,一方面作为一个全局对象;document
是一个跟文档相关的对象,拥有一些操作文档内容的功能;html
元素 和document
元素对象是属于html
文档的DOM
对象。
3、addEventListener
函数的第三个参数是什么?
1. 当为 boolean
时:
- 第三个参数涉及到是冒泡还是捕获;
- 为
true
时是捕获,为false
时是冒泡。
- 为
2. 当为 Object
时:
capture
:Boolean
,表示listener
会在该类型的事件捕获阶段传播到该EventTarget
时触发。once
:Boolean
,表示listener
在添加之后最多只调用一次。如果是true
,listener
会在其被调用之后自动移除。passive
:Boolean
,设置为true
时,表示listener
永远不会调用preventDefault()
。如果listener
仍然调用了这个函数,客户端将会忽略它并抛出一个控制台警告。mozSystemGroup
:只能在XBL
或者是Firefox' chrome
使用,这是个Boolean
,表示listener
被添加到system group
。
4、冒泡和捕获的具体过程
冒泡:当给某个元素绑定了事件之后,这个事件会依次在它的父级元素中被触发; 捕获:从上层向下层传递,与冒泡相反。
<!-- 会依次执行 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
5、有哪些不冒泡的事件
onblur
onfoucs
onmouseenter
onmouseleave
6、原生自定义事件
1. 有哪些自定义事件
- 使用
Event
- 使用
customEvent
- 使用
document.createEvent('customEventName')
和initEvent()
2. 创建自定义事件
1)使用 Event
let myEvent = new Event('my_event_name')
2)使用 customEvent
let myEvent = new CustomEvent('my_event_name', {
detail: {
// 需要传递的参数
// 在监听的回调函数中获取到:event.detail
}
})
3)使用 document.createEvent('CustomEvent')
和initEvent()
let myEvent = document.createEvent('CustomEvent')
myEvent.initEvent(
// event_name 是事件名
// canBubble 是否冒泡
// cancelable 是否可以取消默认行为
)
3. 事件的监听
dom.addEventListener('my_custom_name', function(e) {})
4. 事件的触发
dispatchEvent(myEvent)
5. 案例
// 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
内部函数和闭包
1、什么是闭包
MDN:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)。
简单来说:能够读取其他函数内部变量的函数就是闭包。
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i)
}, 1000)
})(i)
}
2、什么是内部函数
一般来说在一个函数内部定义另外一个函数,这样的函数就是内部函数。
3、闭包的作用?
- 使用闭包可以访问函数中的变量;
- 可以使变量长期保存在内存中。
4、内存泄露
1. 造成内存泄露的情况:
- 循环引用
- 自动类型装箱转换
- 某些
DOM
操作 (44. 闭包
2. 内存泄露解决方案:
- 低于类型转换,可以通过显示类型转换的方式来避免;
- 避免事件导致的循环引用;
- 垃圾箱操作;
- 对于变量的手动删除;
3. 内存泄露是内存占用很大吗?
不是,即使是 1byte
的内存,也叫内存泄露。
4. 程序中提示内存不足,是内存泄露吗?
不是,着一般是无限递归函数调用,导致栈内存溢出。
5. 内存泄露是哪个区域?
堆区。栈区不会泄露
6. 内存泄露的后果?
大多数情况下,后果不是很严重。但是过多的 DOM
操作会使网页执行速度变慢。
7. 跳转网页,内存泄露仍然存在吗?
仍然存在,直到浏览器关闭。
十、EventLoop
的执行过程
1、简述下 EventLoop
的执行过程
- 整个
script
作为一个宏任务进行执行; - 执行中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列;
- 当前宏任务执行完成之后,检测微任务列表,有则进行微任务执行,直到微任务列表全部执行完;
- 执行浏览器的
UI
线程渲染工作; - 检查是否有
web worker
任务,有则执行; - 执行完本轮宏任务,回到第二步,依次循环,直到宏任务和微任务队列都为空。
2、requestAnimationFrame
1. 特征
- 在重新渲染前调用。
- 很可能在宏任务之后不调用。
2. 为什么要在重新渲染前去调用?
因为 rAF
是官方推荐的用来做一些流畅动画所应该使用的 API
,做动画不可避免的会去更改 DOM
,而如果在渲染之后再去更改 DOM
,那就只能等到下一轮渲染机会的时候才能去绘制出来了,这显然是不合理的。
rAF
在浏览器决定渲染之前给你最后一个机会去改变 DOM
属性,然后很快在接下来的绘制中帮你呈现出来,所以这是做流畅动画的不二选择。
3、requestIdleCallback
requestIdleCallback
方法将在浏览器的空闲时段内调用的函数排队。这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应。
1. 渲染有序进行
2. 渲染长期空闲
50ms
可以确保用户在无感知的延迟下得到回应。
4、EventLoop
循环注意点
- 事件循环不一定每轮都伴随着重渲染,但是如果有微任务,一定会伴随着微任务执行。
- 决定浏览器视图是否渲染的因素很多,浏览器是非常聪明的。
requestAnimationFrame
在重新渲染屏幕之前执行,非常适合用来做动画。requestIdleCallback
在渲染屏幕之后执行,并且是否有空执行要看浏览器的调度,如果你一定要它在某个时间内执行,请使用timeout
参数。resize
和scroll
事件其实自带节流,它只在Event Loop
的渲染阶段去派发事件到EventTarget
上。
5、for
循环和 setTimeout
在 for
循环中加入 setTimeout
for (var i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
1. var
改成 let
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i)
}, 1000)
}
2. 使用自执行函数
for (var i = 0; i < 10; i++) {
(function (i) {
setTimeout(() => {
console.log(i)
}, 1000)
})(i)
}
3. for
循环改成 forEach
循环
[1,2,3,4].forEach(item => {
setTimeout(() => {
console.log(item)
}, 1000)
})
4. setTimeout
传参
for (var i = 0; i < arr.length; i++) {
setTimeout((i) => {
console.log(arr[i])
}, 1000, i)
}
5. 直接输出
for (var i = 0; i< 10; i++){
setTimeout(console.log(i),1000);
}
十一、JS
中的 let
、const
、var
1、JS
中有几种定义变量的方法?
let
const
var
class
import
function
2、let
、const
、var
有什么区别?
var | let | const |
---|---|---|
没有块级作用域 | 有块级作用域 | 有块级作用域 |
声明全局变量在 window 下 | (全局属性下) 全局变量不在全局属性下 | 全局变量不在全局属性下 |
重定义变量不会报错 | 会报错 | 会报错 |
声明变量 | 声明变量 | 声明一个常量 |
存在变量提升 | 不存在变量提升 | 不存在变量提升 |
声明之后随时赋值 | 声明之后随时赋值 | 声明之后立即赋值 |
3、const
定义常量可不可以修改?
const
定义基础类型是不可以修改的;const
定义引用类型是可以修改引用类型里面的值。
4、如果我想 const
定义引用类型也不能改变它的值该怎么做?
Object.freeze
;- 代理(
proxy/Object.defineProperty
); - 修改对象的
configurable
、writable
属性。
5、如何在 ES5
的情况下实现 let
和 const
?
1. 实现 let
可以通过自执行函数。
2. 实现 const
可以通过 Object.defineProperty()
实现,设置 writable
。
十二、JS
数组
1、ES6
新增数组方法
Array.from()
、Array.of()
、copyWithin()
、find()
、findIndex()
、fill()
、entries()
、keys()
、values()
、includes()
。
2、ES5
新增数组方法
forEach()
、map()
、filter()
、some()
、every()
、indexOf()
、lastIndexOf()
、reduce()
、reduceRight()
。
3、数组的这些方法,哪些能改变原数组?
copyWithin()
、fill()
、pop()
、push()
、reverse()
、shift()
、sort()
、splice()
。
4、some
和 every
有什么区别?
从中文含义能看出来,some
是某些,every
是每一个,它们都返回一个 Boolean
值。
5、数组里面有 10 万条数据,取第一个元素和第 10 万个元素哪个用时长?
用时基本上一样,因为 js
里面没有数组类型,数组其实也是一个对象,key
和 value
。
6、数组去重你有几种方法?
1. 多层循环遍历法
- 双重
for
循环; - 递归循环。
2. 利用语法自身键不可重复性或者API去重
ES6
Set
去重;- 新建空对象去重;
- 单层循环 +
filter/includes/indexOf
; - 单层循环 +
Map
、Object
去重。
7、for
循环和 forEach
的性能哪个更好一点?
for
循环的性能更好
for
循环没有任何额外的函数调用栈和上下文;forEach
不是普通的for
循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能。
8、sort
排序是按照什么方式来排序的?
默认排序顺序是在将元素转换为字符串,然后比较它们的 UTF-16
代码单元值序列时构建的。
9、多维数组转为一维数组
reduce
递归实现join
和split
实现- 递归遍历
flat
方法toString
和split
实现- 广度优先遍历/深度优先遍历
10、广度优先遍历和深度优先遍历如何实现
1. 深度优先遍历
- 访问顶点
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
}
2. 广度优先遍历
- 创建一个队列,并将开始节点放入队列中;
- 若队列非空,则从队列中取出第一个节点,并检测它是否为目标节点;
- 若是目标节点,则结束搜寻,并返回结果;
- 若不是,则将它所有没有被检测过的字节点都加入队列中;
- 若队列为空,表示图中并没有目标节点,则结束遍历。
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
}
11、实现一个 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
}
12、实现一个数组随机打乱的算法
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)
13、给一串数字增加逗号分隔
1. 正则
num.replace(/(\d)(?=(\d{3})+(\.|$))/g, "$1,")
2. 遍历
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 : "");
}
14、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
1、如何实现一个 sleep
函数(延迟函数)
通过 promise
和 setTimeout
来简单实现
/**
* 延迟函数
* @param {Number} time 时间
*/
function sleep (time = 1500) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(true)
}, time)
})
}
2、promise
构造函数、then
方法、catch
方法、finally
方法哪个异步哪个同步?
promise
构造函数是同步执行的,then
、catch
和 finally
方法是异步执行的。
3、如何取消一个 promise
?
1. 使用 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)
4、多个 promise
如何获取第一个成功promise
?
1. 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)
)
}
2. Promise.any
Promise.any(iterable)
接收一个Promise
对象的集合,当其中的一个promise
成功,就返回那个成功的promise
的值。
缺点:有兼容问题
5、多个 promise
,所有的 promise
都取得返回结果(不管成功/失败都要返回值)
1. Promise.all
改进
和上面原理类似,只不过是当成功的时候不进行操作,当 reject
时进行 resolve
操作
2. Promise.allSettled()
Promise.allSettled(iterable)
返回一个在所有给定的promise
都已经fulfilled
或rejected
后的promise
。
缺点:有兼容问题
6、说说 promise
的静态方法有哪些?
1. Promise.all(iterable)
接收一个 promise
数组对象(可迭代的 promise
实例对象),全部成功时,返回所有 promise
的数组集合;当其中一个失败时,返回当前失败的 promise
对象。
2. Promise.allSettled(iterable)
接收一个 promise
数组对象,全部完成时(不管成功/失败)返回新的 promise
数组集合
3. Promise.any(iterable)
接收一个 promise
数组对象,当其中任何一个成功时,返回成功的 promise
值
4. Promise.race(iterable)
接收一个 promise
数组对象,当其中任意一个成功/失败时,返回该 promise
值
5. Promise.reject(reason)
返回一个状态为失败的 Promise
对象。
6. Promise.resolve(value)
返回一个状态由给定 value
决定的 Promise
对象。
7. Promise.finally(onFinally)
在当前 promise
运行完毕后被调用,无论当前 promise
的状态是完成( fulfilled
)还是失败( rejected
)
8. Promise.try(f)
接收一个函数,返回一个 promise
。
为所有操作提供了统一的处理机制,所以如果想用 then
方法管理流程,最好都用 Promise.try
包装一下。
- 更好的错误处理
- 更好的互操作性
- 易于浏览 Promise-try
7、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 进行错误捕获 |
8、Promise.resolve
有几种情况?
1. 参数是一个 Promise
实例
参数是 Promise
实例,那么 Promise.resolve
将不做任何修改、原封不动地返回这个实例。
2. 参数是一个 thenable
对象
Promise.resolve()
方法会将这个对象转为 Promise
对象,然后就立即执行 thenable
对象的 then()
方法。
3. 参数不是具有 then()
方法的对象,或根本就不是对象
如果参数是一个原始值,或者是一个不具有 then()
方法的对象,则 Promise.resolve()
方法返回一个新的 Promise
对象,状态为 resolved
。
4. 不带有任何参数
直接返回一个 resolved
状态的 Promise
对象。
9、如果 .then
中的参数不是函数,那会怎样?
Promise.resolve(1)
.then(2)
.then(console.log)
// 1
如果 .then
中的参数不是函数,则会在内部被替换为 (x) => x
,即原样返回 promise
最终结果的函数。
10、如果 .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
对象值。
11、.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))
12、.all
是并发的还是串行的?
是并发的,但是返回值和 promise.all
中接收到的数组顺序一样。
13、promise
为什么可以进行链式调用
因为 then
、catch
、finally
方法会返回一个新的 promise
,所以允许我们进行链式调用。
14、async/await
1. 实现原理
async
函数是基于 generator
实现,所以涉及到 generator
相关知识。
在没有async
函数之前,通常使用 co
库来执行 generator
,所以通过 co
我们也能模拟 async
的实现。
2. 简单实现
1)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();
});
}
2)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
1、JSON.stringify
定义:将一个 JavaScript
对象或值转换为 JSON
字符串。
参数:有三个参数
JSON.stringify(value[, replacer [, space]])
replacer
replacer
参数可以是一个函数或者一个数组。 作为函数,它有两个参数,键(key
)和值(value
),它们都会被序列化。replacer
是一个数组,数组的值代表将被序列化成JSON
字符串的属性名。space
space
参数用来控制结果字符串里面的间距。 如果是一个数字, 则在字符串化时每一级别会比上一级别缩进多这个数字值的空格; 如果是一个字符串,则每一级别会比上一级别多缩进该字符串。
2、JSON.parse
定义:用来解析 JSON
字符串。
参数:有两个参数
JSON.parse(text[, reviver])
reviver
转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值。
特性
- 转换值如果有
toJSON()
方法,该方法定义什么值将被序列化。 - 非数组对象的属性不能保证以特定的顺序出现在序列化后的字符串中。
- 布尔值、数字、字符串的包装对象在序列化过程中会自动转换成对应的原始值。
undefined
、任意的函数以及symbol
值,在序列化过程中会被忽略(出现在非数组对象的属性值中时)或者被转换成null
(出现在数组中时)。函数、undefined
被单独转换时,会返回undefined
,如JSON.stringify(function(){})
orJSON.stringify(undefined)
。- 对包含循环引用的对象(对象之间相互引用,形成无限循环)执行此方法,会抛出错误。
- 所有以
symbol
为属性键的属性都会被完全忽略掉,即便 replacer 参数中强制指定包含了它们。 Date
日期调用了toJSON()
将其转换为了string
字符串(同Date.toISOString()
),因此会被当做字符串处理。NaN
和Infinity
格式的数值及null
都会被当做null
。- 其他类型的对象,包括
Map/Set/WeakMap/WeakSet
,仅会序列化可枚举的属性。
十六、==
、===
和 Object.is()
1、区别
==
两边值类型不同的时候,先进行类型转换,在比较===
不进行类型转换,直接值比较Object.is(val1, val2)
判断两个值是否为同一值
2、==
类型转换是怎么转换的?
- 如果类型不同,进行类型转换
- 判断比较的是否是
null
或者是undefined
,如果是,返回true
- 判断类型是否为
string
或者number
,如果是,将string
转换为number
- 判断其中一方是否为
boolean
,如果是,将其中一方转为number
在进行判断 - 判断其中一方是否为
object
,且另外一方是string
、number
、symbol
,如果是,将object
转为原始类型进行判断(valueOf()
方法) - 如果有一个是
NaN
,则直接返回false
- 如果两个都是对象,则比较是否指向同一个对象
3、[] == ![]
的值为什么?
答案:为 true
转换步骤
- ! 运算符优先级最高,
![]
会被转换为false
,因此此时为[] == false
- 根据第四条,其中一方为
boolean
,把boolean
转为number
,所以此时为[] == 0
- 再根据第五条,把数组
[]
转为原始类型,调用数组的toString()
方法,[].toString() = ''
,所以此时为'' == 0
- 再根据第三条,把
string
转为number
,''
转为number
为 0,所以此时0 == 0
- 两边数据类型相同
0 == 0
,为true
4、Object.is() 判断两值相等的情况
不会进行强制类型转换
- 都是
undefined
- 都是
null
- 都是
true
或false
- 都是相同长度的字符串且相同字符按相同顺序排列
- 都是相同对象(意味着每个对象有同一个引用)
- 都是数字且
- 都是
+0
- 都是
-0
- 都是
NaN
- 或都是非零而且非
NaN
且为同一个值
- 都是
十七、防抖和节流
1、什么是防抖和节流
防抖:是多次执行改为最后一次执行 节流:是将多次执行改为每隔一段时间执行
2、简单实现防抖和节流
1. 防抖实现
思路:
触发高频事件后 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))
2. 节流实现
高频事件触发,但在 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
1、三者区别
cookie
用来保存登录信息,大小限制为4KB
左右localStorage
是Html5
新增的,用于本地数据存储,保存的数据没有过期时间,一般浏览器大小限制在5MB
sessionStorage
接口方法和localStorage
类似,但保存的数据的只会在当前会话中保存下来,页面关闭后会被清空。
名称 | 生命期 | 大小限制 | 与服务器通信 | 是否可以跨域 |
---|---|---|---|---|
cookie | 一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie ,默认是关闭浏览器后失效 | 4KB | 每次都会携带在 HTTP 头中,如果使用 cookie 保存过多数据会带来性能问题 | 一般不可,相同 domain 下可以允许接口请求携带 cookie |
localStorage | 除非被清除,否则永久保存 | 5MB | 仅在浏览器中保存,不与服务器通信 | 不可 |
sessionStorage | 仅在当前会话下有效,关闭页面或浏览器后被清除 | 5MB | 仅在浏览器中保存,不与服务器通信 | 不可 |
2、localStorage
进行怎么进行跨域存储?
localStorage
是不可以进行跨域操作的,但是想进行跨域操作可以使用 postMessage
,websocket
进行变相的跨域操作。
十九、浏览器跨域问题
1、什么是浏览器同源策略?
同源策略是一个重要的安全策略,它用于限制一个 origin
的文档或者它加载的脚本如何能与另一个源的资源进行交互,它能帮助阻隔恶意文档,减少可能被攻击的媒介。
所谓同源策略,是指只有在地址的:
- 协议名
- 域名
- 端口名
均一样的情况下,才允许访问相同的 cookie
、localStorage
,以及访问页面的 DOM
或是发送 Ajax
请求。
2、没有同源策略限制有哪些危险场景?
ajxa
请求Dom
的查询 同源策略确实能规避一些危险,不是说有了同源策略就安全,只是说同源策略是一种浏览器最基本的安全机制,毕竟能提高一点攻击的成本。
3、为什么浏览器会禁止跨域?
- 跨域只存在浏览器端,因为浏览器的形态很开放,需要对它进行限制。
- 同源策略用于保护用户信息安全,防止恶意窃取数据(
ajax
同源策略、Dom
同源策略)。
4、跨域有哪些解决方式?
jsonp
cors
postMessage
websocket
Node
中间件代理(两次跨域)nginx
反向代理window.name + iframe
location.hash + iframe
document.domain + iframe
5、CORS
常用的配置有哪些?
Access-Control-Allow-Origin
允许的域名Access-Control-Allow-Methods
允许的http
请求方法Access-Control-Allow-Headers
支持的请求头Access-Control-Allow-Credentials
是否发送cookie
Access-Control-Max-Age
以秒为单位的缓存时间
6、CORS
跨域的判定流程
- 浏览器先判断是否同源,若同源,直接发送数据,否则,发送跨域请求;
- 服务器收到跨域请求后,根据自身配置返回对应的文件头;
- 浏览器根据收到的响应头里的
Access-Control-Allow-origin
字段进行匹配,若无该字段说明不允许跨域,报错,有该字段进行比对,判断是否可以跨域。
7、什么是简单请求?
简单请求是指满足以下条件的:
- 使用
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
对象。
8、非简单请求
对服务器有特殊要求的请求(简单请求之外就是非简单请求)。
例如:请求方式是 put
、delete
,Content-Type
的类型是 application/json
。
非简单请求会在正式通信前使用 options
发起一个预检请求,询问服务器当前的域名是否在服务器允许的名单之中,以及使用哪些头信息字段等。
9、有哪些方法可一嵌入跨源的资源?
script
标签,嵌入跨域脚本;link
标签,嵌入css
;img
标签,嵌入图片;video/audio
标签,嵌入视频、音频;object/embed/applet
标签,嵌入svg
/图片 等;svg
标签,嵌入svg
;- 通过
@font-face
嵌入字体; - 通过
iframe
嵌入资源
10、手动实现一个 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
的继承方式
1、原型链继承 prototype
子类型的原型为父类型的一个实例对象。
Child.prototype = new Parent()
优点:
- 继承方式简单
- 父类新增方法、属性,子类都能访问到 缺点:
- 无法实现多继承
- 来自父类的所有属性被所有实例共享
- 要想为子类新增属性和方法,必须要在Child.prototype = new Parent() 之后,因为会被覆盖
- 创建子类时,不能像父类传递参数
2、构造函数继承 call
在子类型构造函数中通用 call()
调用父类型构造函数
function Child(name, age, price) {
Parent.call(this, name, age) // 相当于: this.Parent(name, age)
}
优点:
- 原型链继承中子类实例共享父类引用属性的问题
- 创建子类实例时,可以向父类传递参数
- 可以实现多继承(call多个父类对象) 缺点:
- 实例并不是父类的实例,只是子类的实例
- 只能继承父类的实例属性和方法,不能继承原型属性和方法
- 无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
3、原型链+构造函数的组合继承 prototype + call
调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用。
function Child (name, age, price) {
Parent.call(this, name, age)
}
Child.prototype = new Parent()
Child.prototype.constructor = Child//组合继承也是需要修复构造函数指向的
优点:
- 可以继承实例属性/方法,也可以继承原型属性/方法
- 不存在引用属性共享问题
- 可传参 缺点:
- 调用了两次父类构造函数,生成了两份实例
4、组合继承优化1
通过父类原型和子类原型指向同一对象,子类可以继承到父类的公有方法当做自己的公有方法,而且不会初始化两次实例方法/属性,避免的组合继承的缺点。
function Child (name, age, price) {
Parent.call(this, name, age)
}
Child.prototype = Parent.prototype
优点:
- 不会调用了两次父类构造函数 缺点:
- 没办法辨别是实例是子类还是父类创造的,子类和父类的构造函数指向是同一个。
5、组合继承优化2
借助原型可以基于已有的对象来创建对象,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
6、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)
优点:
- 简单继承
二十一、排序算法
1、冒泡排序
简单来说就是相邻两个元素进行对比,按照你需要的排序方式(升序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;
}
2、快速排序
选择一个基准,将比基准小的放左边,比基准小的放在右边(基准处在中间位置)
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));
}
3、选择排序
首先从原始数组中找到最小的元素,并把该元素放在数组的最前面,然后再从剩下的元素中寻找最小的元素,放在之前最小元素的后面,直到排序完毕
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;
}
4、插入排序
从第二个元素开始(假定第一个元素已经排序了),取出这个元素,在已经排序的元素中从后向前进行比较,如果该元素大于这个元素,就将该元素移动到下一个位置,然后继续向前进行比较,直到找到小于或者等于该元素的位置,将该元素插入到这个位置后.重复这个步骤直到排序完成
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;
}
5、归并排序
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(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;
}
6、希尔排序
利用步长来进行两两元素比较,然后缩减步长在进行排序。 说明:希尔排序的实质是分组插入排序,该方法又称缩小增量排序。该方法的基本思想是:先将整个待排元素序列分割为若干个子序列(由相隔某个‘增量’的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,带这个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况)效率是很高的,因此希尔排序在时间效率上有较大的提高。 与插入排序的不同之处:它会优先比较距离较远的元素
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
}
7、各类排序算法比较
二十二、时间复杂度、空间复杂度
1、如何去衡量不同算法之间的优劣呢?
主要还是从算法所占用的「时间」和「空间」两个维度去考量。
- 时间维度:是指执行当前算法所消耗的时间,我们通常用「时间复杂度」来描述。
- 空间维度:是指执行当前算法需要占用多少内存空间,我们通常用「空间复杂度」来描述。
2、时间复杂度
1. 表示方法
「 大O符号表示法 」,即 T(n) = O(f(n))
时间复杂度的公式是: T(n) = O( f(n) )
,其中 f(n)
表示每行代码执行次数之和,而 O
表示正比例关系,这个公式的全称是:算法的渐进时间复杂度。
2. 常见的复杂度量级
• 常数阶 O(1)
• 对数阶 O(logN)
• 线性阶 O(n)
• 线性对数阶 O(nlogN)
• 平方阶 O(n²)
• 立方阶 O(n³)
• K次方阶 O(n^k)
• 指数阶 (2^n)
3. O(1)
无论代码执行了多少行,只要是没有循环等复杂结构,那这个代码的时间复杂度就都是 O(1)
var i = 1;
var j = 2;
++i;
j++;
var m = i + j;
4. O(n)
for
循环里面的代码会执行 n
遍,因此它消耗的时间是随着n的变化而变化的,因此这类代码都可以用 O(n)
来表示它的时间复杂度。
for(i=1; i<=n; ++i)
{
j = i;
j++;
}
5. 对数阶 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)
6. O(nlogN)
将时间复杂度为 O(logn)
的代码循环N遍的话,那么它的时间复杂度就是 n * O(logN)
,也就是了 O(nlogN)
。
for(m=1; m<n; m++)
{
i = 1;
while(i<n)
{
i = i * 2;
}
}
7. 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++;
}
}
3、空间复杂度
空间复杂度是对一个算法在运行过程中临时占用存储空间大小的一个量度,同样反映的是一个趋势,我们用 S(n)
来定义。
1. 常见的复杂度量级
空间复杂度比较常用的有:O(1)、O(n)、O(n²)
。
2. O(1)
如果算法执行所需要的临时空间不随着某个变量 n
的大小而变化,即此算法空间复杂度为一个常量,可表示为 O(1)
var i = 1;
var j = 2;
++i;
j++;
var m = i + j;
代码中的 i
、j
、m
所分配的空间都不随着处理数据量变化,因此它的空间复杂度 S(n) = O(1)
3. O(n)
var arr = [1, 2, 3]
for(i=1; i<=arr.lemgth; ++i)
{
j = i;
j++;
}
第一行定义了一个数组出来,这个数据占用的大小为 n
,这段代码的 2-6
行,虽然有循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,即 S(n) = O(n)
二十三、接口请求
1、AJAX
1、简单实现一个 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();
}
}
}
}
2、ajax
的 readyState
的状态
0
未初始化,还没有调用open()
方法1
启动,已经调用open()
方法,但是没有调用send()
方法2
发送,已经调用send()
方法,但是尚未接收响应3
接收,已经接收到部分响应数据4
完成,已经接收到全部响应数据
2、Axios
Axios
本质上也是对原生 XHR
的封装,只不过它是 Promise
的实现版本,符合最新的 ES
规范
1. 特性:
- 从
node.js
创建http
请求 - 支持
Promise API
- 客户端支持防止
CSRF
- 提供了一些并发请求的接口(重要,方便了很多的操作)
3、Fetch
Fetch API
提供了一个 JavaScript
接口,用于访问和操纵 HTTP
管道的部分,例如请求和响应。它还提供了一个全局 fetch()
方法,该方法提供了一种简单,合理的方式来跨网络异步获取资源。
1. 优点
- 语法简洁,更加语义化
- 基于标准
Promise
实现,支持async/await
- 同构方便,使用
isomorphic-fetch
2. 缺点
Fetch
请求默认是不带cookie
的,需要设置fetch(url, {credentials: 'include'})
- 服务器返回 400,500 错误码时并不会
reject
,只有网络错误这些导致请求不能完成时,fetch
才会被reject
。
二十四、new
操作符
1、new
的实现流程
- 1.创建一个新对象;
- 2.将构造函数的作用域赋给新对象(因此
this
就指向了这个新对象); - 3.执行构造函数中的代码(为这个新对象添加属性);
- 4.返回新对象。
- 5.将构造函数的
prototype
关联到实例的__proto__
2、如何实现一个 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()
需要兼容实现
1、网页全屏
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);
}
}
}
2、取消网页全屏
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();
}
}
3、检测是否全屏
/**
* 检查是否全屏
* @return {[Boolean]} [是否全屏,为 true 没有全屏,false 全屏]
*/
function checkFullScreenValue () {
return !document.fullscreenElement &&
!document.mozFullScreenElement && !document.webkitFullscreenElement && !document.msFullscreenElement
}
二十六、Map
、WeakMap
和 set
、WeakSet
有什么区别?
WeakMap
和WeakSet
都是弱引用
1、什么是弱引用
弱引用是指不能确保其引用的对象不会被垃圾回收器回收的引用,换句话说就是可能在任意时间被回收。
弱引用随时都会消失,遍历机制无法保证成员的存在
2、Set
- 遍历顺序:插入顺序
- 没有键只有值,可认为键和值两值相等
- 添加多个
NaN
时,只会存在一个NaN
- 添加相同的对象时,会认为是不同的对象
- 添加值时不会发生类型转换(
5 !== "5"
) keys()
和values()
的行为完全一致,entries()
返回的遍历器同时包括键和值且两值相等
3、weakSet
作用
- 和
Set
结构类似,成员值只能是对象 - 储存
DOM
节点:DOM
节点被移除时自动释放此成员,不用担心这些节点从文档移除时会引发内存泄漏 - 临时存放一组对象或存放跟对象绑定的信息:只要这些对象在外部消失,它在
WeakSet
结构中的引用就会自动消 - 成员都是弱引用,垃圾回收机制不考虑
WeakSet
结构对此成员的引用 - 成员不适合引用,它会随时消失,因此
ES6
规定WeakSet
结构不可遍历 - 其他对象不再引用成员时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于
WeakSet
结构中
4、Map
- 遍历顺序:插入顺序
- 对同一个键多次赋值,后面的值将覆盖前面的值
- 对同一个对象的引用,被视为一个键
- 对同样值的两个实例,被视为两个键
- 键跟内存地址绑定,只要内存地址不一样就视为两个键
- 添加多个以
NaN
作为键时,只会存在一个以NaN
作为键的值 Object
结构提供字符串—值的对应,Map
结构提供值—值的对应
5、WeakMap
- 和
Map
结构类似,成员键只能是对象 - 储存
DOM
节点:DOM
节点被移除时自动释放此成员键,不用担心这些节点从文档移除时会引发内存泄漏 - 部署私有属性:内部属性是实例的弱引用,删除实例时它们也随之消失,不会造成内存泄漏
- 成员键都是弱引用,垃圾回收机制不考虑
WeakMap
结构对此成员键的引用 - 成员键不适合引用,它会随时消失,因此
ES6
规定WeakMap
结构不可遍历 - 其他对象不再引用成员键时,垃圾回收机制会自动回收此成员所占用的内存,不考虑此成员是否还存在于
WeakMap
结构中 - 一旦不再需要,成员会自动消失,不用手动删除引用
- 弱引用的只是键而不是值,值依然是正常引用
- 即使在外部消除了成员键的引用,内部的成员值依然存在
6、Object
转为 Map
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj))
二十七、Proxy
1、语法
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
2、Handler
对象常用的方法
方法 | 描述 |
---|---|
handler.has() | in 操作符的捕捉器。 |
handler.get() | 属性读取操作的捕捉器。 |
handler.set() | 属性设置操作的捕捉器。 |
handler.deleteProperty() | delete 操作符的捕捉器。 |
handler.ownKeys() | Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。 |
handler.apply() | 函数调用操作的捕捉器。 |
handler.construct() | new 操作符的捕捉器 |
3、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 //已撤销
二十八、执行上下文
1、执行上下文的类型?
1. 全局执行上下文:
一个程序中只能存在一个全局执行上下文。
这是默认的、最基础的执行上下文。不在任何函数中的代码都位于全局执行上下文中。它做了两件事:
- 创建一个全局对象,在浏览器中这个全局对象就是
window
对象。 - 将
this
指针指向这个全局对象。
2. 函数执行上下文:
可以有无数个函数执行上下文。
每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都拥有自己的执行上下文,但是只有在函数被调用的时候才会被创建。
3. Eval
函数执行上下文:
js
的 eval
函数执行其内部的代码会创建属于自己的执行上下文, 很少用而且不建议使用。
2、执行上下文的特点
- 单线程;
- 同步执行,从上往下顺序执行;
- 全局上下文只有一个,也就是
window
对象; - 函数执行上下文没有数量限制;
- 函数只有在调用的时候才会被创建,每调用一次就会产生一个新的执行上下文环境。
3、执行上下文的生命周期
1. 创建阶段
- 创建变量对象:首先初始化函数的参数
arguments
,提升函数声明和变量声明。 - 创建作用域链:作用域链是在变量对象之后创建的。作用域链本身包含变量对象。作用域链用于解析变量。当被要求解析变量时,
JavaScript
始终从代码嵌套的最内层开始,如果最内层没有找到变量,就会跳转到上一层父作用域中查找,直到找到该变量。 - 确定
this
指向:确定this
的指向。
2. 执行阶段
- 执行变量赋值。
- 函数引用。
- 执行其他代码。
3. 回收阶段
- 执行上下文出栈
- 等待虚拟机回收执行上下文
4、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上下文环境
二十九、实现一些特殊函数
1、一次性函数
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() // 没有输出,不会再次执行
2、延迟函数(沉睡函数)
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()
3、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})
4、前端生成 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
)
1、V8
内存限制
- 64 位系统可用 1.4G 内存
- 32 位系统可用 0.7G 内存
2、V8
内存管理
JS
对象都是通过V8
进行分配管理内存的process.memoryUsage()
返回一个对象,包含了Node
进程的内存占用信息
3、内存占用结构图
var a = {name:‘yuhua’};
这句代码会做如下几步:- 将这句代码放入“代码区域
Code Segment
” - 将变量
a
放入“栈(Stack
):本地变量、指针” - 将
{name:‘yuhua’}
放入“HeapTotal
(堆):对象,闭包”
- 将这句代码放入“代码区域
- 注意:基本数据类型都在栈中,引用类型都在堆中
4、为何限制内存大小
- 因为
V8
垃圾收集工作原理导致的,1.4G 内存完全一次垃圾收集需要 1s 以上 - 这个垃圾回收这段时间(暂停时间)成为
Stop The World
,在这期间,应用的性能和响应能力都会下降
5、V8
的垃圾回收机制
V8
是基于分代的垃圾回收- 不同代垃圾回收机制也不一样,采用的算法不一样
- 按存货的时间分为新生代和老生代
6、分代
- 年龄小的是新生代,由
From
区域和To
区域两个区域组成 - 在 64 位系统里,新生代内存是 32M,
From
区域和To
区域各占 16M - 在 32 位系统里,新生代内存是 16M,
From
区域和To
区域各占 8M - 年龄大的是老生代,默认情况下:
- 64 位系统下老生代内存是 1400M
- 32 位系统下老生代内存是 700M
7、新生代采用 Scavenge
算法
Scavenge
为新生代采用的算法,是一种采用复制的方式实现的垃圾回收算法。
新生代扫描的时候是一种广度优先的扫描策略
它将内存分为 from
和 to
两个空间。每次 gc
,会将 from
空间的存活对象复制到 to
空间。然后两个空间角色对换(又称反转)。
该算法是牺牲空间换时间,所以适合新生代,因为它的对象生存周期较短。
1. 过程
- 新生代区域一分为二,每个 16M,一个使用,一个空闲
- 开始垃圾回收的时候,会检查
FROM
区域中的存活对象,如果还活着,拷贝到TO
空间,所有存活对象拷贝完后,清空(释放)FROM
区域 - 然后FROM和To区域互换
2. 特点
- 新生代扫描的时候是一种广度优先的扫描策略
- 新生代的空间小,存活对象少
- 当一个对象经理多次的垃圾回收依然存活的时候,生存周期比较差的对象会被移动到老声带,这个移动过程被称为晋升或升级
- 经历过 5 次以上的回收还存在
TO
的空间使用占比超过 25%,或者超大对象- 浏览器的
memory
中可以通过拍快照看变量是否被垃圾回收 - 置为
undefined
或null
都能将引用计数减去 1
8、老生代采用 Mark-Sweep
和 Mark-Compact
1. 基础
- 老生代垃圾回收策略分为两种
mark-sweep
标记清除- 标记活着的对象,虽然清楚在标记阶段没有标记的对象,只清理死亡对象 会出现的问题:清除后内存不连续,碎片内存无法分配
mark-compact
标记整理- 标记死亡后会对对象进行整理,活着的左移,移动完成后清理掉边界外的内存(死亡的对象)
- 老生代空间大,大部分都是活着的对象,
GC
耗时比较长 - 在
GC
期间无法想听,STOP-THE-WORLD
V8
有一个优化方案,增量处理,把一个大暂停换成多个小暂停INCREMENT-GC
- 也就是把大暂停分成多个小暂停,每暂停一小段时间,应用程序运行一会,这样垃圾回收和应用程序交替进行,停顿时间可以减少到1/6左右
2. 过程
假设有10个大小的内存,内存占用了6个,
1)Mark-Sweep
模式垃圾回收:
- 那么会给每个对象做上标记:
A b C d E f 空 空 空 空
//对上面每个对象做上标记,大写表示活着,小写表示死了
//这时候,会存在一个问题,就是内存碎片无法使用,因为小写的内存没有跟后面空空空空的内存放在一起,不能使用
- 这时候小写(死)的都会被干掉,只保留大写(活)的,导致的问题就是内存碎片无法使用
2)Mark-Compact
模式垃圾回收
- 将活的左移
A C E b d f 空 空 空 空
- 然后回收死了的区域
A C E 空 空 空 空 空 空 空
9、三种算法的对比
回收算法 | Mark-Sweep | Mark-Compact | Scavenge |
---|---|---|---|
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少 | 少 | 双倍空间(无碎片) |
是否移动对象 | 否 | 是 | 是 |
V8
老生代主要用Mark-Sweep
,因为Mark-Compact
需要移动对象,执行速度不快。空间不够时,才会用Mark-Compact
三十三、设计模式
1、设计原则:
1. 单一职责原则(SRP
)
一个对象或方法只做一件事情。
2. 最少知识原则(LKP
)
应当尽量减少对象之间的交互。
3. 开放-封闭原则(OCP
)
软件实体(类、模块、函数)等应该是可以 扩展的,但是不可修改
2、策略模式
策略模式是指对一系列的算法定义,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。 优点:
- 策略模式利用组合、委托等技术和思想,可以避免很多if条件语句
- 策略模式提供了开放-封闭原则,使代码更容易理解和拓展
示例:
- 绩效等级和薪资计算奖金为
- 表单验证,通常会涉及到多个字段有效性判断
3、缓存代理模式
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果,提供效率以及节省开销。
缓存代理,就是将前面使用的值缓存下来,后续还有使用的话,就直接拿出来用。
4、工厂模式
工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。
简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用。
工厂方法模式的本意是将实际创建对象的工作推迟到子类中,工厂方法模式就是将这个大厂拆分出各个小厂,每次添加新的产品让小厂去生产,大厂负责指挥就好了。
抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。
5、单例模式
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
确保了只有一个实例
- 因为只有唯一实例,所以节省了系统资源,记住创建和销毁也需要浪费内存资源
- 避免了对资源的多重占用,比如数据库的连接
- 资源共享
前端应用场景:
- 浏览器的
window
对象。在JavaScript
开发中,对于这种只需要一个的对象,往往使用单例实现。 - 遮罩层、登陆浮窗等。
6、代理模式
为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式主要有三种:保护代理、虚拟代理、缓存代理
7、迭代器模式
迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。
JS
中数组的 map
forEach
已经内置了迭代器
8、发布-订阅者模式
也称作观察者模式,定义了对象间的一种一对多的依赖关系,当一个对象的状态发 生改变时,所有依赖于它的对象都将得到通知。
JS中的事件就是经典的发布-订阅模式的实现
9、命令模式
用一种松耦合的方式来设计程序,使得请求发送者和请求接收者能够消除彼此之间的耦合关系
命令(command
)指的是一个执行某些特定事情的指令
三十四、函数&自执行函数
1、自执行函数特点
- 函数表达式与函数声明不同,函数名只在该函数内部有效,并且此绑定是常量绑定。
- 对于一个常量进行赋值,在
strict
模式下会报错,非strict
模式下静默失败。 IIFE
中的函数是函数表达式,而不是函数声明。
2、函数类型
- 函数声明
- 函数表达式
- 函数构造器创建
1. 函数声明(FD
)
- 有一个特定的名称
- 在源码中的位置:要么处于程序级(
Program level
),要么处于其它函数的主体(FunctionBody
)中 - 在进入上下文阶段创建
- 影响变量对象
- 以下面的方式声明
function funName () {}
2. 函数表达式(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'
3. 函数构造器创建的函数
我们将它与 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" 未定义
}
3、如何创建一个函数不需要 () 就可以执行
- 创建对象
- 对象里面表达式定义自执行函数
var foo = {
bar: function (x) {
return x % 2 != 0 ? 'yes' : 'no';
}(1)
};
foo.bar // 'yes'
4、为什么有些要加 ()
有些可以不加?
当函数不在表达式的位置的时候,分组操作符圆括号是必须的——也就是手工将函数转化成 FE
。
如果解析器知道它处理的是 FE
,就没必要用圆括号。
5、具名函数表达式
当函数表达式 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
上。最后一步是从父作用域链中移除那个特定的对象。
6、自执行函数示例
// 例一
+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
攻击
1、XSS
攻击
1. 概念
XSS(Cross Site Scripting)
:跨域脚本攻击。
2. 原理
不需要你做任何的登录认证,它会通过合法的操作(比如在 url
中输入、在评论框中输入),向你的页面注入脚本(可能是 js
、hmtl
代码块等)。
3. 防范
- 编码;对于用户输入进行编码。
- 过滤;移除用户输入和事件相关的属性。(过滤
script
、style
、iframe
等节点) - 校正;使用
DOM Parse
转换,校正不配对的DOM
标签。 HttpOnly
。
4. 分类
- 反射型(非持久):点击链接,执行脚本
- 存储型(持久):恶意输入保存数据库,其他用户访问,执行脚本
- 基于
DOM
:恶意修改DOM
结构,基于客户端
2、CSRF
攻击
1. 概念
SRF(Cross-site request forgery)
:跨站请求伪造。
2. 原理
- 登录受信任网站
A
,并在本地生成Cookie
。(如果用户没有登录网站A
,那么网站B
在诱导的时候,请求网站A
的api
接口时,会提示你登录)。 - 在不登出
A
的情况下,访问危险网站B
(其实是利用了网站A
的漏洞)。
3. 防范
token
验证;- 隐藏令牌;把
token
隐藏在http
请求的head
中。 referer
验证;验证页面来源。
3、两者区别
CSRF
:需要用户先登录网站A
,获取cookie
。XSS
:不需要登录。CSRF
:是利用网站A
本身的漏洞,去请求网站A
的api
。XSS
:是向网站A
注入JS
代码,然后执行JS
里的代码,篡改网站A
的内容。
三十六、input
输入框输入即请求后端接口,频繁请求之后怎样确定最后一次接口的返回值?
1、后端返回请求值(最简单)
前端请求接口的时候会把 input 输入框中的值传给后端,此时后端返回接口数据时把前端传入的值返回回去,页面渲染时只需要进行判断即可。
2、终止上一次请求
当再次请求的时候把上次的请求终止掉:
ajax
:abort()
axios
:CancelToken
fetch
:AbortController
百度用的就是这种取消请求的方式 js:ss1.bdstatic.com/5eN1bjq8AAU…
3. 定义一个全局 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
1、定义
rem(font size of the root element)
是指相对于根元素的字体大小的单位。
1rem
等于根元素 htm
的 font-size
,即只需要设置根元素的 font-size
,其它元素使用 rem
单位时,设置成相应的百分比即可。
2、如何实现
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();
3、以 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
1、dns-prefetch
域名转化为 ip
是一个比较耗时的过程,dns-prefetch
能让浏览器空闲的时候帮你做这件事。尤其大型网站会使用多域名,这时候更加需要 dns
预取。
//来自百度首页
<link rel="dns-prefetch" href="//m.baidu.com">
2、prefetch
prefetch
一般用来预加载可能使用的资源,一般是对用户行为的一种判断,浏览器会在空闲的时候加载 prefetch
的资源。
<link rel="prefetch" href="http://www.example.com/">
3、preload
和 prefetch
不同,prefecth
通常是加载接下来可能用到的页面资源,而 preload
是加载当前页面要用的脚本、样式、字体、图片等资源。所以 preload
不是空闲时加载,它的优先级更强,并且会占用 http
请求数量。
<link rel='preload' href='style.css' as="style" onload="console.log('style loaded')"
as
值包括
script
style
image
media
document
onload
方法是资源加载完成的回调函数
4、defer
和 async
//defer
<script defer src="script.js"></script>
//async
<script async src="script.js"></script>
defer
和 async
都是异步(并行)加载资源,不同点是 async
是加载完立即执行,而 defer
是加载完不执行,等到所有元素解析完再执行,也就是 DOMContentLoaded
事件触发之前。
因为 async
加载的资源是加载完执行,所以它比不能保证顺序,而 defer
会按顺序执行脚本。
三十九、浏览器渲染过程
1、浏览器渲染过程如下
- 解析
HTML
,生成DOM
树 - 解析
CSS
,生成CSSOM
树 - 将
DOM
树和CSSOM
树结合,生成渲染树(Render Tree
) Layout
(回流):根据生成的渲染树,进行回流(Layout
),得到节点的几何信息(位置,大小)Painting
(重绘):根据渲染树以及回流得到的几何信息,得到节点的绝对像素Display
:将像素发送给GPU
,展示在页面上。(这一步其实还有很多内容,比如会在GPU
将多个合成层合并为同一个层,并展示在页面中。而css3
硬件加速的原理则是新建合成层)
2、何时触发回流和重绘
1. 回流
- 添加或删除可见的
DOM
元素 - 元素的位置发生变化
- 元素的尺寸发生变化(包括外边距、内边框、边框大小、高度和宽度等)
- 内容发生变化,比如文本变化或图片被另一个不同尺寸的图片所替代。
- 页面一开始渲染的时候(这肯定避免不了)
- 浏览器的窗口尺寸变化(因为回流是根据视口的大小来计算元素的位置和大小的)
2. 重绘
- 回流一定会触发重绘
- 当页面中元素样式的改变并不影响它在文档流中的位置时(例如:
color
、background-color
、visibility
等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
3、如果避免触发回流和重绘
1. css
- 避免使用
table
布局。 - 尽可能在
DOM
树的最末端改变class
。 - 避免设置多层内联样式。
- 将动画效果应用到
position
属性为absolute
或fixed
的元素上 - 避免使用
CSS
表达式(例如:calc()
) CSS3
硬件加速(GPU
加速)transform
opacity
filters
Will-change
2. JavaScript
- 避免频繁操作样式,最好一次性重写
style
属性,或者将样式列表定义为class
并一次性更改class
属性,修改style
的cssText
属性或者修改元素的className
值。 - 避免频繁操作
DOM
,创建一个documentFragment
,在它上面应用所有DOM
操作,最后再把它添加到文档中 - 也可以先为元素设置
display: none
,操作结束后再把它显示出来。因为在display
属性为none
的元素上进行的DOM
操作不会引发回流和重绘 - 避免频繁读取会引发回流/重绘的属性,如果确实需要多次使用,就用一个变量缓存起来
- 对具有复杂动画的元素使用绝对定位,使它脱离文档流,否则会引起父元素及后续元素频繁回流。
- 使用
css3
硬件加速,可以让transform
、opacity
、filters
这些动画不会引起回流重绘 。但是对于动画的其它属性,比如background-color
这些,还是会引起回流重绘的,不过它还是可以提升这些动画的性能。
4、硬件加速原理
浏览器接收到页面文档后,会将文档中的标记语言解析为 DOM
树。DOM
树和 CSS
结合后形成浏览器构建页面的渲染树。
渲染树中包含了大量的渲染元素,每一个渲染元素会被分到一个图层中,每个图层又会被加载到 GPU
形成渲染纹理,而图层在 GPU
中 transform
是不会触发 repaint
的,最终这些使用 transform
的图层都会由独立的合成器进程进行处理。
1. 浏览器什么时候会创建一个独立的复合图层呢?
3D
或者CSS transform
<video>
和<canvas>
标签CSS filters
- 元素覆盖时,比如使用了
z-index
属性
3D
和 2D transform
的区别就在于,浏览器在页面渲染前为 3D
动画创建独立的复合图层,而在运行期间为 2D
动画创建。动画开始时,生成新的复合图层并加载为 GPU
的纹理用于初始化 repaint
。然后由 GPU
的复合器操纵整个动画的执行。最后当动画结束时,再次执行 repaint
操作删除复合图层。
2. 使用硬件加速的问题
- 内存。如果
GPU
加载了大量的纹理,那么很容易就会发生内容问题,这一点在移动端浏览器上尤为明显,所以,一定要牢记不要让页面的每个元素都使用硬件加速。 - 使用
GPU
渲染会影响字体的抗锯齿效果。这是因为GPU
和CPU
具有不同的渲染机制。即使最终硬件加速停止了,文本还是会在动画期间显示得很模糊。
四十、JSBridge
1、什么是 JSBridge
JSBridge
是一种 JS
实现的 Bridge
,连接着桥两端的 Native
和 H5
。它在 APP
内方便地让 Native
调用 JS
,JS
调用 Native
,是双向通信的通道。JSBridge
主要提供了 JS
调用 Native
代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等。
2、H5
和 native
的区别
name | H5 | Native |
---|---|---|
稳定性 | 调用系统浏览器内核,稳定性较差 | 使用原生内核,更加稳定 |
灵活性 | 版本迭代快,上线灵活 | 迭代慢,需要应用商店审核,上线速度受限制 |
受网速 影响 | 较大 | 较小 |
流畅度 | 有时加载慢,给用户“卡顿”的感觉 | 加载速度快,更加流畅 |
用户体验 | 功能受浏览器限制,体验有时较差 | 原生系统 api 丰富,能实现的功能较多,体验较好 |
可移植性 | 兼容跨平台跨系统,如 PC 与 移动端,iOS 与 Android | 可移植性较低,对于 iOS 和 Android 需要维护两套代码 |
3、JSBridge
的用途
JSBridge
就像其名称中的『Bridge
』的意义一样,是 Native
和非 Native
之间的桥梁,它的核心是 构建 Native
和非 Native
间消息通信的通道,而且是 双向通信的通道。
双向通信的通道:
JS
向Native
发送消息 : 调用相关功能、通知Native
当前JS
的相关状态等。Native
向JS
发送消息 : 回溯调用结果、消息推送、通知JS
当前Native
的状态等。
4、JSBridge
流程
H5
->通过某种方式触发一个url
->Native
捕获到url
,进行分析->原生做处理->Native
调用H5
的JSBridge
对象传递回调。
实现流程
- 第一步:设计出一个
Native
与JS
交互的全局桥对象 - 第二步:
JS
如何调用Native
- 第三步:
Native
如何得知api
被调用 - 第四步:分析
url-
参数和回调的格式 - 第五步:
Native
如何调用JS
- 第六步:
H5
中api
方法的注册以及格式
5、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
时才通过返回值触发调用。原理基本一样,只是实现方式不同。
1. Native
调 JS
1)安卓
native
调用 js
比较简单,只要遵循:”javascript:
方法名(‘参数,需要转为字符串’)”的规则即可。
mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
@Override public void onReceiveValue(String value) { //这里的value即为对应JS方法的返回值 }
});
2)IOS
Native
通过 stringByEvaluatingJavaScriptFromString
调用 Html
绑定在 window
上的函数。
2. JS
调 Native
1)安卓
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;
}
2)IOS
Native
中通过引入官方提供的 JavaScriptCore
库(iOS7
以上),然后可以将 api
绑定到 JSContext
上(然后 Html
中 JS
默认通过 window.top.*
可调用)。
6、JSBridge
接口实现
JSBridge
的接口主要功能有两个:
调用 Native
(给 Native
发消息) 和 接被 Native
调用(接收 Native
消息)。
1. 消息都是单向的,那么调用 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); // 存储回调
}
};
})();
7、JSBridge
如何引用
1. 由 Native
端进行注入
注入方式和 Native
调用 JavaScript
类似,直接执行桥的全部代码。
优点:桥的版本很容易与 Native
保持一致,Native 端不用对不同版本的 JSBridge
进行兼容;与此同时,
缺点:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript
端在调用接口时,需要优先。
2. 由 JavaScript
端引用
直接与 JavaScript
一起执行。
优点:JavaScript
端可以确定 JSBridge
的存在,直接调用即可;
缺点:如果桥的实现方式有更改,JSBridge
需要兼容多版本的 Native Bridge
或者 Native Bridge
兼容多版本的 JSBridge
。
四十一、web worker
1、什么是 web worker
?有哪些好处?有哪些问题?
Web Worker
就是为 JavaScript
创造多线程环境,允许主线程创建 Worker
线程,将一些任务分配给后者运行。在主线程运行的同时,Worker
线程在后台运行,两者互不干扰。等到 Worker
线程完成计算任务,再把结果返回给主线程。
好处:
好处就是,一些计算密集型或高延迟的任务,被 Worker
线程负担了,主线程(通常负责 UI
交互)就会很流畅,不会被阻塞或拖慢。
问题:
Worker
线程一旦新建成功,就会始终运行,不会被主线程上的活动(比如用户点击按钮、提交表单)打断。这样有利于随时响应主线程的通信。但是,这也造成了 Worker
比较耗费资源,不应该过度使用,而且一旦使用完毕,就应该关闭。
2、使用 web worker
有哪些限制?
1. 同源限制
分配给 worker
的脚本文件,必须与主线程脚本文件同源。
2. DOM
限制
worker
线程无法读取主线程所在网页的 DOM
对象,无法使用 document
、window
、parent
这些对象,可以使用 navigator
和 location
对象。
3. 通信限制
worker
线程和主线程不再同一个上下文环境中,不能直接通信,必须通过消息完成。
4. 脚本限制
worker
线程不能执行 alert
方法和 confirm
方法,但是可以发出 ajax
请求。
5. 文件限制
worker
线程无法读取本地文件,不能打开文件系统,所加载的脚本,必须来自网络,不能是 file://
文件。
3、worker
线程怎样监听主线程的消息的?如何发送消息的?worker
线程又是如何关闭的?
Worker
线程内部需要有一个监听函数,监听 message
事件。
// 监听
self.addEventListener('message', function (e) {
// 发送消息
self.postMessage('You said: ' + e.data);
}, false);
关闭 worker
线程
1)主线程关闭 worker
线程
worker.terminate()
2)worker
线程关闭
self.close()
4、worker
线程如何加载其他脚本?
importScript('scripts.js')
importScript('scripts1.js', 'scripts2.js')
5、主线程和 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
1、为什么需要 webSocket
?有什么特点?
1. 优势:
- 支持双向通信,实时性更强;
- 更好的二进制支持;
ws
客户端与服务端数据交换时,数据包头部较小,更好的控制开销;- 支持拓展。
2. 特点:
- 建立在
TCP
协议之上,服务器端的实现比较容易。 - 与
HTTP
协议有着良好的兼容性。默认端口也是 80 和 443,并且握手阶段采用HTTP
协议,因此握手时不容易屏蔽,能通过各种HTTP
代理服务器。 - 数据格式比较轻量,性能开销小,通信高效。
- 可以发送文本,也可以发送二进制数据。
- 没有同源限制,客户端可以与任意服务器通信。
- 协议标识符是
ws
(如果加密,则为wss
),服务器网址就是URL
。
2、webSocket
的链接状态?
0 (WebSocket.CONNECTING)
正在链接中1 (WebSocket.OPEN)
已经链接并且可以通讯2 (WebSocket.CLOSING)
连接正在关闭3 (WebSocket.CLOSED)
连接已关闭或者没有链接成功