如何应对金九银十----JavaScript高频面试题
函数式编程
函数是编程是一种设计思想,就像面向对象编程也是一种设计思想。函数式编程总的来说就是用尽量用函数组合来进行编程,先声明函数,然后调用函数的每一步都有返回值,将具体的每一步逻辑运算抽象,封装在函数中。再将函数组合来编写程序。
this指向的问题
- 箭头函数体内的this对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。
- 全局环境中:在浏览器环境严格模式中(区别于node),this默认指向window。立即执行函数,默认的定时器等函数,this也是指向window。
- 对象函数中:如果函数作为对象的一个属性被调用时,函数中的this指向该对象。
- 构造函数创建的实例的属性指向构造函数的prototype。
- dom节点中:特别在是react中jsx语法中,我们通常要改变dom的this指向,不然获取不到指定的执函数。所以通常需要在函数声明使用bind绑定事件。
// html
<button id="btn">myBtn</button>
// js
var name = 'window'
var btn = document.getElementById('btn')
btn.name = 'dom'
var fn = function (){console.log(this.name)}
btn.addEventListener('click', f()) // this指向 btn
btn.addEventListener('click', f.bind(obj)) // this指向 window
箭头函数不能作为构造函数使用的原因构造函数是通过 new 关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定 this 的过程,而箭头函数没有自己的 this。因此不能使用箭头作为构造函数,也就不能通过 new 操作符来调用箭头函数。
箭头函数和普通函数的区别
1.语法更加简洁、清晰
2.箭头函数不会创建自己的this
3.箭头函数继承而来的this指向永远不变
4.call()
/.apply()
/.bind()
无法改变箭头函数中this的指向
5.箭头函数不能作为构造函数使用
6.箭头函数没有自己的arguments
7.箭头函数没有原型prototype
8.箭头函数不能用作Generator
函数,不能使用yeild关键字箭头函数不能作为构造函数?构造函数是通过new 关键字来生成对象实例,生成对象实例的过程也是通过构造函数给实例绑定this 的过程,而箭头函数没有自己的this。 因此不能使用箭头作为构造函数,也就不能通过new 操作符来调用箭头函数。
js的数据类型
string
number
null
undefined
object
boolean
bigInt
Symbol
js判断数据类型的方法
- 对于对象、数组、null返回的值是object。比如typeof {},typeof [],typeofnull返回的值都是object。
console.log(Object.prototype.toString.call(a) === '[object String]');
// true- constructor在类继承时会出错
- instanceof 后面一定要是对象类型
Js几种循环的区别
JavaScript的4种数组遍历方法:for VS forEach() VS for/in VS for/offor
循环,forEach的区别
简单地说,for of是遍历数组最可靠的方式,它比for循环简洁,并且没有for in和forEach()那么多奇怪的特例。for of的缺点是我们取索引值不方便,而且不能这样链式调用forEach(). forEach()。使用for/of获取数组索引,可以这样写:
for (const [i, v] of arr.entries()) {
console.log(i, v);
}
函数的 this
for,for/in与for/of会保留外部作用域的this。对于forEach, 除非使用箭头函数,它的回调函数的 this 将会变化。
空元素
for/in与forEach会跳过空元素
Async/Await
forEach()不能与 Async/Await 及 Generators 很好的”合作”。不能在forEach回调函数中使用 await。
普通对象和Map的区别
1.key值不限制
2.可直接遍历常规对象里,为了遍历keys、values和entries,你必须将它们转换为数组,如使用Object.keys()、Object.values()和Object.entries(),或使用for … in,另外for … in循环还有一些限制:它仅仅遍历可枚举属性、非Symbol属性,并且遍历的顺序是任意的。但Map可直接遍历,且因为它是键值对集合,所以可直接使用for…of或forEach来遍历。这点不同的优点是为你的程序带来更高的执行效率。
介绍下Set、Map、WeakSet和WeakMap的区别?
Set
1.成员不能重复
2.只有健值,没有健名,有点类似数组。
3. 可以遍历,方法有add, delete,has
weakSet
成员都是对象成员都是弱引用,随时可以消失。
可以用来保存DOM节点,不容易造成内存泄漏不能遍历,方法有add, delete,has
Map
本质上是健值对的集合,类似集合可以遍历,方法很多,可以干跟各种数据格式转换
weakMap
直接受对象作为健名(null除外),不接受其他类型的值作为健名健名所指向的对象,不计入垃圾回收机制
不能遍历,方法同get,set,has,delete
IE如何直接引用ES6语法
broswer.js
如何描述Promise
Promise 是一个对象,它代表了一个异步操作的最终完成或者失败。
本质上 Promise 是一个函数返回的对象,我们可以在它上面绑定回调函数,这样我们就不需要在一开始把回调函数作为参数传入这个函数了。
不同于“老式”的传入回调,在使用 Promise 时,会有以下约定:
- 在本轮事件循环运行完成之前,回调函数是不会被调用的。
- 即使异步操作已经完成(成功或失败),在这之后通过 then()添加的回调函数也会被调用。
- 通过多次调用 then() 可以添加多个回调函数,它们会按照插入顺序进行执行。
Promise 很棒的一点就是链式调用(chaining)。
web Worker
在HTML页面中,如果在执行脚本时,页面的状态是不可相应的,直到脚本执行完成后,页面才变成可相应。web worker是运行在后台的js,独立于其他脚本,不会影响页面性能。并且通过postMessage将结果回传到主线程。这样在进行复杂操作的时候,就不会阻塞主线程了。如何创建web worker:
1. 检测浏览器对于web worker的支持性
2. 创建web worker文件(js,回传函数等)
3. 创建web worker对象
1.主线程里面引入和监听
var worker = new Worker('work.js');
worker.onmessage = function (event) {
console.log('Received message ' + event.data); doSomething();
}
向worker线程里面进行事件推送
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});
2.worker线程
worker线程内部需要一个监听函数,监听message事件
self.addEventListener('message', function (e) {
self.postMessage('You said: ' + e.data);
}, false);
self代表子线程自身,即子线程的全局对象,等同于下面两种方法:
// 写法一
this.addEventListener('message', function (e) {
this.postMessage('You said: ' + e.data);
}, false);
// 写法二
addEventListener('message', function (e) {
postMessage('You said: ' + e.data);
}, false);
也可以使用self.onmessage指定
self.addEventListener('message', function (e) {
var data = e.data;
//主线程发送过来的数据
switch (data.cmd) {
case 'start':
self.postMessage('WORKER STARTED: ' + data.msg);
break;
case 'stop':
self.postMessage('WORKER STOPPED: ' + data.msg);
self.close();
// Terminates the worker,用于在worker内部关闭自身
break;
default:
self.postMessage('Unknown command: ' + data.msg);
};
}, false);
async 和 await
实现一个简单的async/await
如上,我们掌握了Generator函数的使用方法。async/await语法糖就是使用Generator函数+自动执行器来运作的。 我们可以参考以下例子
// 定义了一个promise,用来模拟异步请求,作用是传入参数++
function getNum(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(num+1)
}, 1000)
})
}
//自动执行器,如果一个Generator函数没有执行完,则递归调用
function asyncFun(func){
var gen = func();
function next(data){
var result = gen.next(data);
if (result.done) return result.value;
result.value.then(function(data){
next(data);
});
} next();
}
// 所需要执行的Generator函数,内部的数据在执行完成一步的promise之后,再调用下一步
var func = function* (){
var f1 = yield getNum(1);
var f2 = yield getNum(f1);
console.log(f2);
};
asyncFun(func);
async/await和promise性能差异
当调用a()函数时,这些事情同步发生,b()函数产生一个promise对象,调用then方法,Promise会在将来的某个时刻resolve,也就是把then里的回调函数添加到回调链。这样,a()函数就执行完了,在这个过程中,a()函数并不会暂停,因此在异步函数resolve的时候,a()的作用域已经不存在了,那要如何生成包含a()的堆栈信息呢? 为了解决这个问题,JavaScripts引擎要做一些额外的工作;它会及时记录并保存堆栈信息。对于V8引擎来说,这些堆栈信息随着Promise在Promise链中传递,这样c()函数在需要的时候也能获取堆栈信息。但是这无疑造成了额外的开销,会降低性能;保存堆栈信息会占用额外的内存。
使用await的时候,无需存储堆栈信息,因为存储b()到a()的指针的足够了。当b()函数执行的时候,a()函数被暂停了,因此a()函数的作用域还在内存可以访问。如果b()抛出一个错误,堆栈通过指针迅速生成。如果c()函数抛出一个错误,堆栈信息也可以像同步函数一样生成,因为c()是在a()中执行的。不论是b()还是c(),我们都不需要去存储堆栈信息,因为堆栈信息可以在需要的时候立即生成。而存储指针,显然比存储堆栈更加节省内存
Promise 如何实现串行
function runPromiseByQueue(myPromises) {
myPromises.reduce(
(previousPromise, nextPromise) => previousPromise.then(
() => nextPromise()
),
Promise.resolve()
);
}
// 栈如何实现队列
var CQueue = function(){
this.inStack = []
this.outStack = []
}
CQueue.prototype.appendTail = function(value){
// 加入元素 可以直接向instack里面push
this.inStack.push(value)
}
CQueue.prototype.deleteHead = function(){
const {inStack,outStack} = this;
if(outStack.length){
return outStack.pop()
//两个栈 一个instack 一个outstack 我想要删除队列头部的元素 用一个栈是不行的
//要把instack里的元素倒进outstack里,然后取第一个元素
while(inStack.length){
outStack.push(inStack.pop());
}
return outStack.pop() || -1
}
}
var obj = new CQueue()
obj.appendTail(1)
obj.appendTail(2)
obj.appendTail(3)
console.log(obj) // [1,2]
obj.deleteHead()
console.log(obj) // [2]
如果一个请求 50ms中返回,阻塞了UI的渲染 要怎么处理(分片,多进程?)
setTimeout
由于长任务执行时间长,会阻塞主线程,用户能感觉到页面卡顿,所以我们经常会采用 setTimeout 把长任务推入任务队列,等到同步代码执行完成后再处理长任务:
const t = setTimeout(() =>{
clearTimeout(t);
// 长任务
},0)function ts(gen) {
if (typeof gen === 'function') gen = gen();
if (!gen || typeof gen.next !== 'function') return;
return function next() {
const start = performance.now();
const res = null;
do {
res = gen.next();
} while (!res.done && performance.now() - start < 25);
if (res.done) return;
setTimeout(next);
};
}
上面代码核心思想是:通过yield关键字可以将任务暂停执行,从而让出主线程的控制权;通过定时器可以将“未完成的任务”重新放在任务队列中继续执行。上面代码中,做了一个 do while:如果当前任务执行时间低于 25ms 则多个任务一起执行,否则作为一个任务执行,一直到 res.done = true 为止。代码核心思想:通过 yield 关键字可以将任务暂停执行,并让出主线程的控制权;通过setTimeout将未完成的任务重新放在任务队列中执行
webWorker
<input type="text" id="ipt" value="" />
<div id="result"></div>
<script>
const ipt = document.querySelector('#ipt');
const worker = new Worker('worker.js');
ipt.onchange = function() {
// 通过postMessage发送消息
worker.postMessage({ number: this.value });
};
// 通过onmessage接收消息
worker.onmessage = function(e) {
document.querySelector('#result').innerHTML = e.data;
};
</script>
// self 类似主线程中的window
self.onmessage = function(e) {
self.postMessage(e.data.number * 2);
};
如何求set类型的长度
new Set([2,2,3]).size
介绍下TypeScript的工具类型
- Partial,Required, Readonly
- Record<K extends keyof any, T> 的作用是将 K 中所有的属性的值转化为 T 类型
- Pick<T, K extends keyof T> 的作用是将某个类型中的子属性挑出来,变成包含这个类型部分属性的子类型。
- Exclude<T, U> 的作用是将某个类型中属于另一个的类型移除掉。
- Extract<T, U> 的作用是从 T 中提取出 U 。
- Omit<T, K extends keyof any> 的作用是使用 T 类型中除了 K 类型的所有属性,来构造一个新的类型。
Websocket怎么建立连接的?
websocket的连接建立过程:
- 客户端发送GET 请求, upgrade
- 服务器给客户端 switching protocol
- 就进行了webSocket的通信了
前端模块化的意义
一个项目里面,不论大小,如果一个JS文件上百上千行,多个JS文件之间依赖性,你想想该有多痛苦。同时一直推崇“高内聚低耦合”的原则意义在于
1. 提高开发效率,有利于多人协同开发
2. 解决文件依赖问题,无需考虑引用包顺序
3. 避免全局变量污染
4. 方便代码的复用和后期的维护
CommonJS和ES6Module
- 因为CommonJS的require语法是同步的,所以就导致了CommonJS模块规范只适合用在服务端,而ES6模块无论是在浏览器端还是服务端都是可以使用的,但是在服务端中,还需要遵循一些特殊的规则才能使用;
- CommonJS 模块输出的是一个值的拷贝,而ES6 模块输出的是值的引用;
- CommonJS 模块是运行时加载,而ES6 模块是编译时输出接口,使得对JS的模块进行静态分析成为了可能;
- 因为两个模块加载机制的不同,所以在对待循环加载的时候,它们会有不同的表现。CommonJS遇到循环依赖的时候,只会输出已经执行的部分,后续的输出或者变化,是不会影响已经输出的变量。而ES6模块相反,使用import加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值;
- 关于模块顶层的this指向问题,在CommonJS顶层,this指向当前模块;而在ES6模块中,this指向undefined;
- 关于两个模块互相引用的问题,在ES6模块当中,是支持加载CommonJS模块的。但是反过来,CommonJS并不能requireES6模块,在NodeJS中,两种模块方案是分开处理的。
闭包的应用
闭包:闭包是指有权访问另一个函数作用域中的变量函数;
应用场景:
- setTimeout里传参数
- 回调
- 封装私有函数私有变量
- 循环计数
let 与 const 的表现在很多情况下都相似于 var ,然而在循环中就不是这样。在 for-in 与 for-of 循环中, let 与 const 都能在每一次迭代时创建一个新的绑定,这意味着在循 环体内创建的函数可以使用当前迭代所绑定的循环变量值(而不是像使用 var 那样,统一使 用循环结束时的变量值)。这一点在 for 循环中使用 let 声明时也成立,不过在 for 循 环中使用 const 声明则会导致错误。
ES6新特性
- 类
- 模块化
- 箭头函数
- 函数参数默认值
- 模板字符串
- 解构赋值
- 延展操作符
- 对象属性简写
- Promise
- Let与Const
proxy
Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等),等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 就像在目标对象之间的一个代理,任何对目标的操作都要经过代理。代理就可以对外界的操作进行过滤和改写。
Proxy是构造函数,它有两个参数target和handler。
target是用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler是一个对象,其属性是当执行一个操作时定义代理的行为的函数。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
}, set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
Proxy只有一个静态方法revocable(target, handler)可以用来创建一个可撤销的代理对象。两个参数和构造函数的相同。它返回一个包含了所生成的代理对象本身以及该代理对象的撤销方法的对象。
Reflect
Reflect是一个内置的对象,它提供拦截 JavaScript 操作的方法。Reflect不是一个函数对象,因此它是不可构造的。
迭代器和生成器
迭代器:JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就需要一种统一的接口机制,来处理所有不同的数据结构。遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的作用:
为各种数据结构,提供一个统一的、简便的访问接口;
使得数据结构的成员能够按某种次序排列
ES6创造了一种新的遍历命令for...of循环,Iterator接口主要供for...of消费。
迭代器的工作原理
创建一个指针对象,指向数据结构的起始位置。
第一次调用next方法,指针自动指向数据结构的第一个成员
接下来不断调用next方法,指针会一直往后移动,直到指向最后一个成员
每调用next方法返回的是一个包含value和done的对象,{value: 当前成员的值,done: 布尔值}
value表示当前成员的值,done对应的布尔值表示当前的数据的结构是否遍历结束。
当遍历结束的时候返回的value值是undefined,done值为true
手写一个
function myIterator(arr) {
let nextIndex = 0
return {
next: function() {
return nextIndex < arr.length
? { value: arr[nextIndex++], done: false }
: { value: undefined, value: true }
}
}
}
let arr = [1, 4, 'ads']
// 准备一个数据
let iteratorObj = myIterator(arr)
console.log(iteratorObj.next())
// 所有的迭代器对象都拥有next()方法,会返回一个结果对象
console.log(iteratorObj.next())
console.log(iteratorObj.next())
console.log(iteratorObj.next())
① for of循环不支持遍历普通对象对象的Symbol.iterator属性,指向该对象的默认遍历器方法。当使用for of去遍历某一个数据结构的时候,首先去找Symbol.iterator,找到了就去遍历,没有找到的话不能遍历,提示Uncaught TypeError: XXX is not iterable
② 当使用扩展运算符(…)或者对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法
生成器
1.概念
- Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同
- 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
- Generator 函数除了状态机,还是一个遍历器对象生成函数。
- 可暂停函数(惰性求值), yield可暂停,next方法可启动。每次返回的是yield后的表达式结果
2.特点
- function关键字与函数名之间有一个星号;
- 函数体内部使用yield表达式,定义不同的内部状态
3.yield
表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function* generatorExample() {
console.log('开始执行')
let result = yield 'hello'
console.log(result)
yield 'generator'
}
let MG = generatorExample()
MG.next()
MG.next(11)
// 开始执行
// 11
// {value: "generator", done: false}
4.Generator的异步的应用
function* sendXml() {
// url为next传参进来的数据
let url = yield getNews('http://localhost:3000/news?newsId=2');
//获取新闻内容yield getNews(url);
//获取对应的新闻评论内容,只有先获取新闻的数据拼凑成url,才能向后台请求
}
function getNews(url) {
$.get(url, function (data) {
console.log(data);
let commentsUrl = data.commentsUrl;
let url = 'http://localhost:3000' + commentsUrl;
// 当获取新闻内容成功,发送请求获取对应的评论内容
// 调用next传参会作为上次暂停是yield的返回值
sx.next(url);
})
}
let sx = sendXml();
// 发送请求获取新闻内容sx.next();
与 Iterator 接口的关系
任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。
var myIterable ={};
myIterable[Symbol.iterator]=function*(){
yield 1;
yield 2;
yield 3;
};
[...myIterable]
// [1, 2, 3]
上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被…运算符遍历了。Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
// some code
}
var g = gen();
gSymbol.iterator === g
// true
上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。
ES6新增方法
Object.assign()
; 复制一个对象
Object.keys()
; 得到一个对象的所有属性;
Object.values()
; 得到一个对象的所有可枚举属性值;
Object.entries()
; 得到一个对象的键值对数组;
Object.fromEntries()
; entries的逆操作;
Array.from
方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)
Array.of
方法用于将一组值,转换为数组。弥补数组构造函数 Array()的不足。因为参数个数的不同,会导致 Array()的行为有差异
find()
方法找到第一个符合条件的成员,没有符合的则返回 undefined
findIndex
方法的用法与 find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
fill()
方法使用给定值,填充一个数组fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置
entries()
,keys()
和 values()
——用于遍历数组,可以用 for...of
循环进行遍历,唯一的区别是 keys()
是对键名的遍历、values()
是对键值的遍历,entries()
是对键值对的遍历
includes()
方法返回一个布尔值
flat()
用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。传参数代表拉平几层默认是一层
flatMap()
只能展开一层数组。方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map()
),然后对返回值组成的数组执行 flat()
方法。该方法返回一个新数组,不改变原数组
// flat()
[1, 2, [3, [4, 5]]].flat()
// [1, 2, 3, [4, 5]]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
//flatMap()
[2, 3, 4].flatMap((x) => [x, x * 2])
//map执行完后是[[2, 4], [3, 6], [4, 8]]
ES7新特性
- 数组
includes()
方法,用来判断一个数组是否包含一个指定的值,根据情况,如果包含则返回true,否则返回false。 - a ** b指数运算符,它与
Math.pow(a, b)
相同。
ES8新特性
async/await
Object.values()
Object.entries()
String
padding
:padStart()
和padEnd()
,填充字符串达到当前长度- 函数参数列表结尾允许逗号
Object.getOwnPropertyDescriptors()
ShareArrayBuffer
和Atomics
对象,用于从共享内存位置读取和写入
ES9新特性
- 异步迭代
Promise.finally()
Rest/Spread
属性- 正则表达式命名捕获组(Regular Expression Named Capture Groups)
- 正则表达式反向断言(lookbehind)
- 正则表达式dotAll模式
- 正则表达式Unicode转义
- 非转义序列的模板字符串
ES10新特性
- 行分隔符(U + 2028)和段分隔符(U + 2029)符号现在允许在字符串文字中,与JSON匹配 更加友好的
JSON.stringify
- 新增了Array的
flat()
方法和flatMap()
方法 - 新增了String的
trimStart()
方法和trimEnd()
方法 Object.fromEntries()
Symbol.prototype.description
String.prototype.matchAll
Function.prototype.toString()
现在返回精确字符,包括空格和注释- 简化
try {}
catch {}
,修改catch
绑定 - 新的基本数据类型
BigInt
globalThis
import()
Legacy RegEx
- 私有的实例方法和访问器
原型链
利用原型让提个引用类型继承另一个引用类型的属性和方法。每个构造函数都一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么假如我们让原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个原型的指针,相应的另一个原型中也包含一个指向另一个构造函数的指针,假如另一个原型又是另一个类型的实例。那么上述关系依然成立,层层递进,构成了实例和原型的链条,这就是原型链的基本概念。
MVC和MVVM
优点:
1、把业务逻辑全部分离到Controller
中,模块化程度高。当业务逻辑变更的时候,不需要变更View和Model,只需要Controller换成另外一个Controller就行了(Swappable Controller)。
2、观察者模式可以做到多视图同时更新。
缺点:
1、Controller
测试困难。因为视图同步操作是由View
自己执行,而View
只能在有UI的环境下运行。在没有UI环境下对Controller
进行单元测试的时候,Controller
业务逻辑的正确性是无法验证的:Controller
更新Model
的时候,无法对View
的更新操作进行断言。
2、View
无法组件化。View
是强依赖特定的Model
的,如果需要把这个View
抽出来作为一个另外一个应用程序可复用的组件就困难了。因为不同程序的的Domain Model
是不一样的
判断数组的方式
Type of
不行instanceof
不能区分数组和对象- 实例化的数组拥有一个
constructor
属性,这个属性指向生成这个数组的方法。 console.log(a.constructor == Array)
; // trueconstructor
属性被修改之后,就无法用这个方法判断数组是数组了,除非你能保证不会发生constructor
属性被改写的情况,否则用这种方法来判断数组也是不靠谱的。- 另一个行之有效的方法就是使用
Object.prototype.toString
方法来判断,每一个继承自Object
的对象都拥有toString
的方法。 Array.isArray(a)
; // true
var、let 和 const 区别的实现原理是什么
-
var
与let
是可以声明变量,const
不能声明变量,只能声明只读的常量。 -
var
声明的变量不存在块级作用域,他在全局内有效。let
与const
的声明只在其所在的代码块中有效。 -
let/const
不能在同一个作用域中声明相同变量/常量,var
可以多次重复声明。 -
var
存在变量提升,所以var能先使用在声明,但是let
const
必须先声明再使用。 -
let/const
存在暂时性死区。 -
const
声明时必须初始化赋值,一旦声明,其声明赋值的值就不允许改变,更不可以重复声明。
原理
JavaScript中的函数运行在它们被定义的作用域里,而不是它们被执行的作用域里。
- var会直接在栈内存里预分配内存空间,然后等到实际语句执行的时候,再存储对应的变量,如果传的是引用类型,那么会在堆内存里开辟一个内存空间存储实际内容,栈内存会存储一个指向堆内存的指针。
- let是不会在栈内存里预分配内存空间,而且在栈内存分配变量时,做一个检查,如果已经有相同变量名存在就会报错。
- const也不会预分配内存空间,在栈内存分配变量时也会做同样的检查。不过const存储的变量是不可修改的,对于基本类型来说你无法修改定义的值,对于引用类型来说你无法修改栈内存里分配的指针,但是你可以修改指针指向的对象里面的属性。
总结
块级作用域的原理,栈是一种LIFO的结构。先进后出。在运行for的时候,压栈,把for循环和对应的变量i压入栈内存。运行结束时便将栈内存弹出。里面的变量i也不复存在了。变量提升其实就是在编译阶段把var声明的变量注入到变量环境中,而块级作用域的实现其实是通过不同的块来保存块中使用let、const 声明的变量,通过栈的机制来处理不同的块。执行上下文对理解 this,闭包有很大的作用
移动端
viewport标签作用
在每个html页面的header部分常常会看见一个meta标签. 这个标签中最常写的就是下面的内容:
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
这个标签内的内容表示啥意思呢?
name为viewport表示供移动设备使用。
content定义了viewport的属性。
width表示移动设设备下显示的宽度为设备宽度(device-width)
height表示移动设设备下显示的宽度为设备宽度。
user-scalable表示用户缩放能力, no表示不允许。
initial-scale表示设备与视口的缩放比率。
maximum和minimum分别表示缩放的最大最小值, 要注意的是, maximum必须必minimum大。
上面的meta标签就是告诉浏览器, 不要在移动端显示的时候缩放。
JavaScript 与 Native之间的互相调用
注入api的形式
安卓:
// js 在全局window上声明一个函数供安卓调用
window.callAndroid = function() {
console.log('来自中h5的方法,供native调用')
return "来自h5的返回值"
}
总结:
- 4.4 之前Native通过loadUrl来调用js方法,只能让某个js方法执行,但是无法获取该方法的返回值 2. 4.4 之后,通过evaluateJavaScript异步调用js方法,并且能在onReceive中拿到返回值
- 不适合传输大量数据
-
- mWebView.loadUrl(“javascript: 方法名”) 函数需在UI线程运行,因为mWebView为UI控件,会阻塞UI线程
js 调用原生代码
window.foo('test'); // 返回 'foo:test'
注意:ios7 以前 js无法调用native方法,ios7之后可以引入第三方提供的 JavaScriptCore 库
总结:
- ios7 才出现这种方式,在这之前js无法直接调用Native,只能通过JSBridge方式调用
- JS 能调用到已经暴露的api,并且能得到相应返回值
- ios原生本身是无法被js调用的,但是通过引入官方提供的第三方“JavaScriptCore”,即可开发api给JS调用
ios开发自带两种webview控件
UIWebview(ios8 以前的版本,建议弃用)
- 版本较老
- 可使用JavaScriptCore来注入全局自定义对象
- 占用内存大,加载速度慢
WKWebview
- 版本较新
- 加载速度快,占用内存小
- js使用全局对象window.webkit.messageHandlers.{NAME}.postMessage 来调用native的方法
原生和h5 的另一种通讯方式:最广为流行的方法 JSBridge-桥协议
JSBridge 是广为流行的Hybrid 开发中JS和Native一种通信方式,简单的说,JSBridge就是定义Native和JS的通信,Native只通过一个固定的桥对象调用JS,JS也只通过固定的桥对象调用native,基本原理是:
h5 --> 通过某种方式触发一个url --> native捕获到url,进行分析 -->原生做处理 --> native 调用h5的JSBridge对象传递回调
为什么要用JSBridge
上面我们看到native已经和js实现通信,为什么还要通过url scheme 的这种jsBridge方法呢
1. Android4.2 一下,addJavaScriptInterface方式有安全漏洞
2. ios7以下,js无法调用native
3. url scheme交互方式是一套现有的成熟方案,可以兼容各种版本
- jsBridge是一种交互理念一种协议,而上述url scheme则是其中的一种实现方式,所以也就是说,就算后面实现变为了
addJavaScriptInterface、JavaScriptCore,也是一样和JSBridge交互
url scheme 介绍
-
url scheme是一种类似于url的链接,是为了方便app直接互相调用设计的:具体为:可以用系统的 OpenURI
打开类似与url的链接(可拼入参数),然后系统会进行判断,如果是系统的 url
scheme,则打开系统应用,否则找看是否有app注册中scheme,打开对应app,需要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为
weixin:// -
本文JSBridge中的url
scheme则是仿照上述的形式的一种具体位置app不会注册对应的scheme,而是由前端页面通过某种方式触发scheme(如用
iframe.src),然后native用某种方法捕获对应的url触发事件,然后拿到当前触发url,根据定好的协议(schme://method…),分析当前触发了哪种方法,然后根据定义来实现
实现一个JSBridge
- 设计出一个native与js交互的
全局桥对象
- js如何调用native
- native如何得知api被调用
- 分析 url 参数和回调的格式
- native如何调用js
- h5中api方法的注册以及格式
- 设计一个native与js交互的全局对象 ==> 规定js和native之间的通信必须通过一个h5全局对象JSBridge来实现
// 名称: JSBridge 挂在 window上的一个属性
var JSBridge = window.JSBridge || (window.JSBridge = {});
该对象有如下方法:
registerHandler(String, Function) 注册本地 js 方法,注册后 native可通过 JSBridge调用,注册后会将方法注册到本地变量 messageHandles中
sendHandler(String, JSON, Function) h5 调用原生开放的api,调用后实际上还是本地通过 url scheme触发,调用时会将回调 id 存放到本地变量responseCallbacks 中 _handleMessageFromNative h5 调用native之后的回调通知
参数为 {reposeId: 回调id, responseData: 回调数据}
var JSBridge = {
// 注册本地方法供原生调用
registerHandler: function(method, cb) {
// 会将cb 放入 messageHandlers里面,待原生调用
},
messageHandles: {},
// h5注册方法集合,供native通知后回调调用
// h5 主动调用native,需生成唯一的callbackId
sendHandler: function(mathod, data, succCb, errCb) {
// 内部通过iframe src url scheme 向native发送请求
// 并将对应的回调注册进 responseCallbacks
// native 处理结束后将结果信息通知到h5 通过 _handleMessageFromNative
// h5 拿到返回信息处理 responseCallbacks 里对应的回调
},
responseCallbacks: {},
// 回调集合
// native 通知 h5
_handleMessageFromNative: function(message) {
// 解析 message,然后根据通知类型执行 messageHandles 或 responseCallbacks里的回调
}
}
注意:
1 . native 调用_handleMessageFromNative通知h5,参数为 json 字符串
2 . native 主动调用h5方法时 {methodName: api名, data, callbackId}
methodName: 开放api的名称
data: 原生处理后传递给 h5 参数
需要把回调函数的值 return 出去,供native拿到,
或者再发一个 bridge 回去,方法名是 methodNameSuccess,或者严禁掉,方法名为native生产的callbackId
如:
bridge.register("hupu.ui.datatabupdate", (name) => {
if(name) {
// 再发一个bridge通知原生tab更新成功,,,method 可以为native生成的 callbackId
bridge.send('hupu.ui.datatabsuccess', {})
}
});
- js 如何调用native ==> 通过 sendHandler 方法调用原生
// sendHandler 执行步骤
- 判断是否有回调函数,如果有,生成一个回调函数id,并将id,和对应的回调添加放入回调函数集合 responseCallbacks 中
- 通过特定的参数转换方法,将传入的数据,方法名一起拼接成一个 url scheme,如下:
var param = {
method: 'methodName',
data: {xx: 'xx'},
success: 'successId',
error: 'errorId'
}
// 变成字符串并编码
var url = schme://ecape(JSON.stringify(param))
- 使用内部创建好的iframe来触发scheme(location.href = 可能会造成跳转问题)
创建iframe
var iframe = document.createElment('iframe');
iframe.src = url;document.head.appendChild(iframe);
setTimeout(() => document.head.removeChild('iframe'), 200)
- native 如何得知 api 被调用
- 安卓捕获 url scheme:shouldoverrideurlloading 捕获到url进行分析
- 安卓端也可通过h5的 window.prompt(url, ‘’) 来触发scheme,然后native通过重写webviewClient
的 onJsPrompt 来获取url,然后解析 - ios: 在 UIWebView WKWebview 内发起的所有网络请求,都可以通过 delegate函数在native层得到通知,通过
shouldStartLoadWithRequest
捕获webview中触发的url scheme - 分析url参数和回调的格式
- native已经接收到了js调用的方法,接下来原生应该按照定义好的数据格式来解析数据了,从url中提取出
method
、data
、successId
、errorId
- 根据方法名,再本地寻找对应的方法,接收对应的参数进行执行,执行完毕后,然后通知 h5并携带相应参数
- native如何调用 js (参照上面的native执行js的方法)
- h5调用native后的被动通知
JSBridge._handleMessageFromNative(messageJSON)
,json格式:{responseId, reponseData}
- native 主动调用h5 注册的方法,
JSBridge._handleMessageFromNative(param)
,param
格式为{methodName, data}
,由于是异步不支持批量调用
什么是DOM?
文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。
DOM常见API
parentNode 接口
Node.children
// 返回指定节点的所有Element子节点
Node.firstElementChild
// 返回当前节点的第一个Element子节点
Node.lastElementChild
// 返回当前节点的最后一个Element子节点
Node.childElementCount
// 返回当前节点所有Element子节点的数目。
操作
Node.appendChild(node)
// 向节点添加最后一个子节点
Node.hasChildNodes()
// 返回布尔值,表示当前节点是否有子节点
Node.cloneNode(true)
// 默认为false(克隆节点), true(克隆节点及其属性,以及后代)
Node.insertBefore(newNode,oldNode)
// 在指定子节点之前插入新的子节点
Node.removeChild(node)
// 删除节点,在要删除节点的父节点上操作
Node.replaceChild(newChild,oldChild)
// 替换节点
Node.contains(node)
// 返回一个布尔值,表示参数节点是否为当前节点的后代节点。Node.compareDocumentPosition(node)
// 返回一个7个比特位的二进制值,表示参数节点和当前节点的关系
Node.isEqualNode(noe)
// 返回布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同。
Node.normalize()
// 用于清理当前节点内部的所有Text节点。它会去除空的文本节点,并且将毗邻的文本节点合并成一个。
ChildNode接口
Node.remove()
// 用于删除当前节点
Node.before()
Node.after()
Node.replaceWith()
Document节点的方法
读写方法
document.open()
// 用于新建并打开一个文档
document.close()
// 不安比open方法所新建的文档
document.write()
// 用于向当前文档写入内容
document.writeIn()
// 用于向当前文档写入内容,尾部添加换行符。查找节点document.querySelector(selectors)
// 接受一个CSS选择器作为参数,返回第一个匹配该选择器的元素节点。
document.querySelectorAll(selectors)
// 接受一个CSS选择器作为参数,返回所有匹配该选择器的元素节点。
document.getElementsByTagName(tagName)
// 返回所有指定HTML标签的元素document.getElementsByClassName(className)
// 返回包括了所有class名字符合指定条件的元素
document.getElementsByName(name)
// 用于选择拥有name属性的HTML元素(比如<form>、<radio>、<img>、<frame>、<embed>
和<object>
等)
document.getElementById(id)
// 返回匹配指定id属性的元素节点。
document.elementFromPoint(x,y)
// 返回位于页面指定位置最上层的Element子节点。生成节点
document.createElement(tagName)
// 用来生成HTML元素节点。
document.createTextNode(text)
// 用来生成文本节点
document.createAttribute(name)
// 生成一个新的属性对象节点,并返回它。
document.createDocumentFragment()
// 生成一个DocumentFragment对象事件方法
document.createEvent(type)
// 生成一个事件对象,该对象能被element.dispatchEvent()方法使用document.addEventListener(type,listener,capture)
// 注册事件
document.dispatchEvent(event)
//触发事件
其他
document.hasFocus()
//返回一个布尔值,表示当前文档之中是否有元素被激活或获得焦点。document.adoptNode(externalNode)
//将某个节点,从其原来所在的文档移除,插入当前文档,并返回插入后的新节点。
document.importNode(externalNode, deep)
什么是BOM
浏览器对象模型(BOM)指的是由Web浏览器暴露的所有对象组成的表示模型。BOM与DOM不同,其既没有标准的实现,也没有严格的定义, 所以浏览器厂商可以自由地实现BOM。作为显示文档的窗口, 浏览器程序将其视为对象的分层集合。当浏览器分析文档时, 它将创建一个对象的集合, 以定义文档, 并详细说明它应如何显示。浏览器创建的对象称为文档对象。它是浏览器使用的更大的对象集合的一部分。此浏览器对象集合统称为浏览器对象模型或BOM。BOM层次结构的顶层是窗口对象, 它包含有关显示文档的窗口的信息。某些窗口对象本身就是描述文档和相关信息的对象。BOM提供了一些访问窗口对象的一些方法,我们可以用它来移动窗口位置,改变窗口大小,打开新窗口和关闭窗口,弹出对话框,进行导航以及获取客户的一些信息如:浏览器品牌版本,屏幕分辨率。但BOM最强大的功能是它提供了一个访问HTML页面的一入口——document
对象,以使得我们可以通过这个入口来使用DOM的强大功能!!!window对象是BOM的顶层(核心)对象,所有对象都是通过它延伸出来的,也可以称为window的子对象。由于window是顶层对象,因此调用它的子对象时可以不显示的指明window对象