1. 基础类型和类型检测
简单类型:Undefined, Null, boolean, number, string。 存储结构-栈
复杂类型:Object, Array, Date, Function, RegExp (Symbol, Set, Map)存储结构-堆
基本包装类型:Boolean, Number, String )存储结构-堆
类型检测
1.typeof: 区分不了引用类型(typeof null === Object)
2.instanceof: 繁琐,但还能用于区分拥有原型链的类型
3.constructor: 容易篡改
4.Object.prototype.toString.call():完美(ES6的也能区分)
1. 判断基本类型
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(“abc”);// "[object String]"
Object.prototype.toString.call(123);// "[object Number]"
Object.prototype.toString.call(true);// "[object Boolean]"
2. 判断原生引用类型
**函数类型**
Function fn(){
console.log(“test”);
}
Object.prototype.toString.call(fn); // "[object Function]"
**日期类型**
var date = new Date();
Object.prototype.toString.call(date); // "[object Date]"
**数组类型**
var arr = [1,2,3];
Object.prototype.toString.call(arr); // "[object Array]"
**正则表达式**
var reg = /[hbc]at/gi;
Object.prototype.toString.call(reg); // "[object RegExp]"
**自定义类型**
function Person(name, age) {
this.name = name;
this.age = age;
}
var person = new Person("Rose", 18);
Object.prototype.toString.call(arr); // "[object Object]"
很明显这种方法不能准确判断person是Person类的实例,而只能用instanceof 操作符来进行判断,如下所示:
console.log(person instanceof Person); // true
3. 判断原生JSON对象
var isNativeJSON = window.JSON && Object.prototype.toString.call(JSON);
console.log(isNativeJSON);// 输出结果为”[object JSON]”说明JSON是原生的,否则不是;
2. 类型转换
undefined: undefined 的字面意思就是未定义的值,这个值的语义是,希望表示一个变量最原始的状态,而非人为操作的结果
null: null 的字面意思是空值,这个值的语义是,希望表示 一个对象被人为的重置为空对象,而非一个变量最原始的状态 规定 undefined == null 表示其行为相似性
void 0 === undefined
3. == 与 ===
- === 严格相等
1.类型相等
2.普通类型值相等, 引用类型的地址相等
- == 相同 (带类型转换)
1.undefined == null
2.使用toPrimitive转换成原始值后比较
4. valueOf 和 toString
toString: 将值转换为字符串形式并返回,不同类型的toString方法各有不同
valueOf:返回类型的值
5. 原型和原型链
- 原型
1.每一个Js对象(null除外)在创建的时候都会关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性和方法。
2.__proto__
是对象实例才有的属性,指向对象的原型。
3.prototype
是构造函数才有的属性,该属性指向了一个对象,这个对象正是调用该构造函数而创建的实例的原型。
4.实例的__proto__
属性 和 构造函数的 prototype
都指向该对象原型。
5.Function
的prototype
和__proto__
属性都指向f ()
匿名函数。
6.Object
作为构造函数时,它的prototype
指向Object.prototype
对象原型,作为实例时,他的__proto__
指向匿名函数。我们可以认为Function
实例和Object
实例都是继承于该匿名函数。
7.匿名函数作为“顶级构造函数”,他不需要prototype属性,即prototype=undefined,当作为对象时,他的对象原型是Object.prototype
。
8.Object.prototype
作为“顶级构造对象”,它的__proto__
等于null,表示继承于一个空的对象。它没有prototype
属性。
- 原型链
1.用__proto__
链接起来的就是原型链。原型链用于查找对象上的属性,如果未能从当前的对象上获取到,就会继续从该原型链上查找,直到查到相应的属性。
2.原型链的顶层指向window,严格模式下不会指向window而是undefined
6. Class和继承
- Class
1.ES6之前实例化对象是通过构造函数实现的,ES6后可以通过关键字class创建类(可以认为是一种语法糖)
2.class中的constructor就是在实例化对象调用的构造函数,该构造函数可不写。
3.实例对象必须使用new 关键字生成
4.class不可以当做函数执行
5.class不存在变量提升
6.class中定义的属性和方法都挂在原型上,所有的实例对象都有这些属性和方法。构造函数中定义的是实例的属性和方法。
6.class中可以通过static定义静态方法,静态变量需在类外声明(calss.staticName==staticValue)。静态属性和方法只可以通过class来调用,实例不可调用
- 继承
1.继承属性,方法,静态方法
(1)ES6继承: 通过extends关键字
(2)ES5继承: 通过修改原型链实现继承
2.本质
(1)ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。
(2)ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
- 继承的几种方法:
1.原型链继承:替换子类型的原型
缺点:
(1)包含引用类型的原型属性会被所有实例共享
(2)在创建子类型的实例时,不能向超类型的构造函数中传递参数
2.经典继承(借用构造函数):为了避免实例共享原型属性而带来的技术
缺点:
(1)无法做到函数复用
(2)不能继承超类型在原型上定义的方法
3.组合继承:融合了原型链继承和经典继承,避免了他们的缺陷
缺点:需要调用两次超类型的构造函数
4.原型继承:基于已有的对象创建新的对象
缺点:包含引用类型的原型属性会被所有实例共享
5.寄生式继承:思路和工厂模式类似,即创建一个仅用于继承过程的函数
缺点:无法做到函数复用
6.寄生式组合继承:通过构造函数来继承属性,通过原型链的混成形式来继承方法
主要实现原理:
PersonB.prototype = Object.create(PersonA.prototype)
实现来继承PersonA的原型
当我们通过new关键字实例化的对象身上就有了PersonB自身的属性和方法,也有了PersonA的原型方法
当实例化对象调用某个方法时会先在自身和原型上查找,然后是在_proto_
上一层层查找,这种方式就是原型链。
7. 闭包
简单来说就是函数嵌套函数,内部函数引用来外部函数的变量,从而导致了垃圾回收机制没有生效,变量被保存了下来。
可能产生闭包的二种情况:
1.函数作为返回值,
2.函数作为参数传递
- 优点:
1.可以读取函数内部的变量
2.另一个就是让这些变量的值始终保持在内存中,不会在函数调用后被自动清除,同时这也算是个缺点。(在函数中return一个函数出来)
3.可用于模拟私有变量和方法
- 缺点:
1.消耗内存,影响网页性能,造成页面卡顿等现象。
2.可能会引起内存泄漏(不再用到的内存,但是没有及时释放,就叫做内存泄漏)
8. this
this指的是当前的执行环境
一般时指向window, 严格模式下this绑定到undefined
对象调用函数的情况下,指向调用者
构造函数下,指向实例(比如:Vue中的this指向的是Vue实例)
9. 修改this指向的几种方法
call: call(this, arg1, arg2, ...)
,适用于参数个数确定情况apply: apply(this, firstArg | argArray[])
,适用于参数个数不确定的情况bind: bind(this, firstArg | argArray[])
,返回一个函数,函数内的this指向传入的thiswith: with (expression) { statement }
with语句將某个对象添加到作用域链的顶部(window之下,没有切断作用域链,在expression中找不到定义的,仍会往window上寻找),在严格模式该标签禁止使用
10. new的原理
创建一个空对象,构造函数中的this指向这个空对象
这个新对象的proto设置为即构造函数的prototype
执行构造函数方法,属性和方法被添加到this引用的对象中
如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
11. 实现一个new函数
let _new = function(factory, ...rest) {
let o = {
"__proto__": factory.prototype
}
let res = factory.apply(o, rest)
return typeof res === 'object' ? res : o;
}
12. let、const、var
let和var都用于声明变量,而const必须初始化,且用于声明常量,这个常量指的是普通类型的值不变和复杂类型的内存地址不变。
var存在变量提升,而let,const存在“暂时性死区”,即在变量声明之前就访问变量的话,会直接提示ReferenceError,而不像var那样使用默认值undefined
let,const只有块级作用域,而var只有全局作用域和函数作用域概念
13. 箭头函数
首先语法更简化
不绑定this, 它会捕获其所在(即定义的位置)上下文的this值, 作为自己的this值,这也意味着使用call和apply是无法传递this,第一个参数就是需要传递的参数
不能使用new 关键字,因为箭头函数不是一个构造函数
没有prototype属性
yield 关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作生成器。
arguments,即没有函数的参数arguments,但可以使用剩余参数...args替代
14. Promise详解
Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件(回调地狱)——更合理和更强大。Promise
对象有以下两个特点
1.对象的状态不受外界影响。Promise
对象代表一个异步操作,有三种状态:pending
(进行中)、fulfilled
(已成功)和rejected
(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。
2.一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise
对象的状态改变,只有两种可能:从pending
变为fulfilled
和从pending
变为rejected
。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved
(已定型)。如果改变已经发生了,你再对Promise
对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
- 基本用法
Promise
对象是一个构造函数,用来生成Promise
实例。
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
- 常用方法:
1.Promise.prototype.then()
Promise
实例具有then
方法,也就是说,then
方法是定义在原型对象Promise.prototype
上的。它的作用是为 Promise
实例添加状态改变时的回调函数。
then
方法的第一个参数是resolved
状态的回调函数,第二个参数(可选)是rejected
状态的回调函数。
then
方法返回的是一个新的Promise实例(注意,不是原来那个Promise
实例)。因此可以采用链式写法,即`then方法后面再调用另一个then方法。
promise.then(()=>{
// ...
}).then(()=> {
});
2.Promise.prototype.catch()
Promise.prototype.catch()
方法是.then(null, rejection)
或.then(undefined, rejection)
的别名,用于指定发生错误时的回调函数。
promise.then(()=>{
// ...
}).catch((error)=> {
// 处理 getJSON 和 前一个回调函数运行时发生的错误
console.log('发生错误!', error);
});
下面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then
方法执行中的错误,也更接近同步的写法(try/catch
)。因此,建议总是使用catch()
方法,而不使用then()
方法的第二个参数。
跟传统的try/catch
代码块不同的是,如果没有使用catch()
方法指定错误处理的回调函数,Promise
对象抛出的错误不会传递到外层代码,即不会有任何反应。
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});
// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});
3.Promise.prototype.finally()
finally()
方法用于指定不管 Promise
对象最后状态如何,都会执行的操作。【ES2018】
promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});
上面代码中,不管promise
最后的状态,在执行完then
或catch
指定的回调函数以后,都会执行finally
方法指定的回调函数。
finally
方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise
状态到底是fulfilled
还是rejected
。这表明,finally
方法里面的操作,应该是与状态无关的,不依赖于 Promise
的执行结果。
finally
本质上是then
方法的特例。
4.Promise.all()
Promise.all()
方法用于将多个 Promise
实例,包装成一个新的 Promise
实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()
方法接受一个数组作为参数,p1、p2、p3
都是 Promise
实例,如果不是,就会先调用Promise.resolve
方法,将参数转为 Promise
实例,再进一步处理。另外,Promise.all()
方法的参数可以不是数组,但必须具有 Iterator
接口,且返回的每个成员都是 Promise
实例。
p
的状态由p1、p2、p3
决定,分成两种情况。
(1)只有p1、p2、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1、p2、p3
的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。
5.Promise.race()
Promise.race()
方法同样是将多个 Promise
实例,包装成一个新的 Promise
实例。
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise
实例的返回值,就传递给p
的回调函数。
Promise.race()
是异步操作竞赛,值返回最快的一个
6.Promise.allSettled()
Promise.allSettled()
方法接受一组 Promise
实例作为参数,包装成一个新的 Promise
实例。只有等到所有这些参数实例都返回结果,不管是fulfilled
还是rejected
,包装实例才会结束。【ES2020】
有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()
方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()
方法无法做到这一点。
7.Promise.any()
Promise.any()
方法接受一组 Promise
实例作为参数,包装成一个新的 Promise
实例。只要参数实例有一个变成fulfilled
状态,包装实例就会变成fulfilled
状态;如果所有参数实例都变成rejected
状态,包装实例就会变成rejected
状态。
Promise.any()
跟Promise.race()
方法很像,只有一点不同,就是不会因为某个 Promise
变成rejected
状态而结束。
const promises = [
fetch('/endpoint-a').then(() => 'a'),
fetch('/endpoint-b').then(() => 'b'),
fetch('/endpoint-c').then(() => 'c'),
];
try {
const first = await Promise.any(promises);
console.log(first);
} catch (error) {
console.log(error);
}
上面代码中,Promise.any()
方法的参数数组包含三个 Promise
操作。其中只要有一个变成fulfilled
,Promise.any()
返回的 Promise
对象就变成fulfilled
。如果所有三个操作都变成rejected
,那么await
命令就会抛出错误。
8.Promise.resolve()
有时需要将现有对象转为 Promise
对象,Promise.resolve()
方法就起到这个作用。
const jsPromise = Promise.resolve($.ajax('/whatever.json'));
上面代码将 jQuery
生成的deferred
对象,转为一个新的 Promise
对象。
Promise.resolve()
等价于下面的写法。
Promise.resolve('foo')
// 等价于
new Promise(resolve => resolve('foo'))
9.Promise.reject()
Promise.reject(reason)
方法也会返回一个新的 Promise
实例,该实例的状态为rejected
。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
上面代码生成一个 Promise
对象的实例p
,状态为rejected
,回调函数会立即执行。
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
10.Promise.try()
让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API ,简言之就是,让同步操作也可以像异步一样执行。
const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next
15. ES6新特性
- 严格模式
1.变量必须声明后,才能使用
2.函数的参数不能有同名属性, 否则报错
3.禁止this指向全局对象
4.增加了保留字(比如protected、static和interface)
关于let和const新增的变量声明
变量的解构赋值
字符串的扩展
1.includes():返回布尔值,表示是否找到了参数字符串。
2.startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
3.endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
- 数值的扩展
1.Number.isFinite()用来检查一个数值是否为有限的(finite)。
2.Number.isNaN()用来检查一个值是否为NaN。
函数的扩展,函数参数指定默认值
数组的扩展,扩展运算符
对象的扩展,对象的解构
新增symbol数据类型
1.ES6 引入了一种新的原始数据类型Symbol
,表示独一无二的值。它是 JavaScript
语言的第七种数据类型,前六种是:undefined
、null
、布尔值(Boolean
)、字符串(String
)、数值(Number
)、对象(Object
)。
2.Symbol
值通过Symbol
函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol
类型。凡是属性名属于 Symbol
类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。
let s = Symbol();
- Set 和 Map 数据结构
1.ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。Set 本身是一个构造函数,用来生成 Set 数据结构。
2.Map它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。
- Proxy
1.Proxy
可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
2.Proxy
这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
3.Vue3.0使用了proxy
- Promise
1.Promise
是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。
2.对象的状态不受外界影响。
3.一旦状态改变,就不会再变,任何时候都可以得到这个结果。
- async和await函数
1.async
表示函数里有异步操作,await
表示紧跟在后面的表达式需要等待结果。
2.正常情况下,await
命令后面是一个 Promise
对象。如果不是,会被转成一个立即resolve
的 Promise 对象。
3.async
函数返回一个 Promise
对象,可以使用then
方法添加回调函数。当函数执行的时候,一旦遇到await
就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
- Class
class
跟let、const
一样:不存在变量提升、不能重复声明ES6 的
class
可以看作只是一个语法糖,它的绝大部分功能ES5 都可以做到,新的
class
写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。
- Module
1.ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。
2.import
和export
命令以及export
和export default
的区别
3.export:
(1)export
导出应该是一种接口或是理解为一种定义,而不应该是值
(2)export
导出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
(3)export
命令可以出现在模块的任何位置,只要处于模块顶层就可以。
4.export default:
(1)本质上,export default
就是在Module
上输出一个叫做default
的变量或方法,和export
完全不同,所以它后面不能跟变量声明语句,但表达式,function,class
除外。
5.import:
(1)import
命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
(2)对于export
导出的接口应该使用import { interface1 }
的方式
(3)对于export default
导出的变量应该使用import interface1
的方式
(4)import
命令具有提升效果,会提升到整个模块的头部,首先执行。
(5)如果多次重复执行同一句import
语句,那么只会执行一次,而不会执行多次。
16. CommonJs
CommonJS
是为服务器提供的一种模块形式的优化,CommonJS
模块建议指定一个简单的用于声明模块服务器端的API,并且不像AMD那样尝试去广泛的操心诸如io
,文件系统,约定以及更多的一揽子问题。它有以下特点:主要运用在服务端
js
,如node
全局对象:
global
一个文件就是一个模块,拥有单独的作用域,所有代码都运行在模块作用域,不会污染全局作用域;模块可以多次加载,但只会在第一次加载的时候运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果;(你可以暴露一个时间戳来测试)
模块的加载顺序,按照代码的出现顺序,
同步加载
通过
require
来加载模块:require
基本功能:读取并执行一个JS文件,然后返回该模块的module.exports
对象,如果没有发现指定模块会报错通过
exports
和module.exports
来暴露模块中的内容那么
exports
和module.exports
有什么区别呢?
1.模块内的exports
:为了方便,node
为每个模块提供一个exports
变量,其指向module.exports
,相当于在模块头部加了这句话:var exports = module.exports
,在对外输出时,可以给module.exports
对象添加方法
2.module.exports
方法还可以单独返回一个数据类型(String、Number、Object...
),而 exports
只能返回一个 Object
对象。所有的 exports
对象最终都是通过 module.exports
传递执行,因此可以更确切地说,exports
是给 module.exports
添加属性和方法。
17. CommonJS和ES6模块的区别?
CommonJS模块是运行时加载,ES6 Modules是编译时输出接口
CommonJS输出是值的拷贝;ES6 Modules输出的是值的引用,被输出模块的内部的改变会影响引用的改变
CommonJs导入的模块路径可以是一个表达式,因为它使用的是require()方法;而ES6 - Modules只能是字符串
CommonJS this指向当前模块,ES6 Modules this指向undefined
且ES6 Modules中没有这些顶层变量:arguments、require、module、exports、__filename、__dirname
18. 什么是UMD?AMD和CMD?
UMD是为了让同一个代码模块在使用 CommonJs、CMD 甚至是 AMD 的项目中运行。为了实现兼容,所以有点“丑陋”。
AMD 是 RequireJS 在推广过程中对模块定义的规范化而产出的。
CMD 是 SeaJS 在推广过程中对模块定义的规范化而产出的。
对于依赖的模块,AMD可以提前执行,也可以延迟执行,CMD则是延迟执行。
AMD推崇依赖前置,CMD则推崇就近依赖。(可以说,CMD就是个"懒人")
AMD支持全局require、局部require,但是CMD则不支持全局require,所以CMD没有全局API,而AMD有。
19. 浮点数计算精确度问题
- 因为浮点数在计算机内是以二进制存储和计算的,所以在浮点数上计算会存在精度问题如:
0.1 + 0.2 = 0.30000000000000004
1.0 - 0.9 = 0.09999999999999998
- 解决:
1.使用toFixed
进行“四舍五入”
2.将数扩大至整数,再进行计算
3.使用例如number-precision
等第三库进行计算
20. 最大安全数
- 最大安全值为
2^53-1
最大安全值为-2^53+1
21. Array对象
- 构造函数,Array是 JavaScript 的原生对象,同时也是一个构造函数,可以用它生成新的数组。
// bad
var arr = new Array(1, 2);
// good 字面量法,更好的声明方式
var arr = [1, 2];
- Array.isArray()
这是一个静态方法,返回一个布尔值,表示参数是否为数组。它可以弥补typeof
运算符的不足。
var arr = [1, 2, 3];
typeof arr // "object"
Array.isArray(arr) // true
以下都是实例方法
- Array.valueOf()
valueOf
方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的valueOf
方法不尽一致,数组的valueOf
方法返回数组本身。
var arr = [1, 2, 3];
arr.valueOf() // [1, 2, 3]
- Array.toString()
toString
方法也是对象的通用方法,数组的toString
方法返回数组的字符串形式。
var arr = [1, 2, 3];
arr.toString() // "1,2,3"
var arr = [1, 2, 3, [4, 5, 6]];
arr.toString() // "1,2,3,4,5,6"
- Array.push()
push
方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var arr = [];
arr.push(1) // 1
arr.push('a') // 2
arr.push(true, {}) // 4
arr // [1, 'a', true, {}]
- Array.pop()
pop
方法用于删除数组的最后一个元素,并返回该元素。注意,该方法会改变原数组。
var arr = ['a', 'b', 'c'];
arr.pop() // 'c'
arr // ['a', 'b']
// 对空数组使用pop方法,不会报错,而是返回undefined。
[].pop() // undefined
// push()和pop()结合使用,就构成了“后进先出”的栈结构(stack)。
var arr = [];
arr.push(1, 2);
arr.push(3);
arr.pop();
arr // [1, 2]
// 上面代码中,3是最后进入数组的,但是最早离开数组。
- Array.shift()
shift
方法用于删除数组的第一个元素,并返回该元素。注意,该方法会改变原数组。
var a = ['a', 'b', 'c'];
a.shift() // 'a'
a // ['b', 'c']
// shift方法可以遍历并清空一个数组。
var list = [1, 2, 3, 4, 5, 6];
var item;
while (item = list.shift()) {
console.log(item);
}
list // []
push和shift结合使用,就构成了“先进先出”的队列结构(queue)。
- Array.unshift()
unshift
方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,该方法会改变原数组。
var a = ['a', 'b', 'c'];
a.unshift('x'); // 4
a // ['x', 'a', 'b', 'c']
// unshift方法可以接受多个参数,这些参数都会添加到目标数组头部。
var arr = [ 'c', 'd' ];
arr.unshift('a', 'b') // 4
arr // [ 'a', 'b', 'c', 'd' ]
- Array.join()
join
方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔。
var a = [1, 2, 3, 4];
a.join(' ') // '1 2 3 4'
a.join(' | ') // "1 | 2 | 3 | 4"
a.join() // "1,2,3,4"
// 如果数组成员是undefined或null或空位,会被转成空字符串
[undefined, null].join('#')
// '#'
['a',, 'b'].join('-')
// 'a--b'
// 通过call方法,这个方法也可以用于字符串或类似数组的对象。
Array.prototype.join.call('hello', '-')
// "h-e-l-l-o"
var obj = { 0: 'a', 1: 'b', length: 2 };
Array.prototype.join.call(obj, '-')
// 'a-b'
- Array.concat()
concat
方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变。
['hello'].concat(['world'])
// ["hello", "world"]
['hello'].concat(['world'], ['!'])
// ["hello", "world", "!"]
[].concat({a: 1}, {b: 2})
// [{ a: 1 }, { b: 2 }]
[2].concat({a: 1})
// [2, {a: 1}]
// 除了数组作为参数,concat也接受其他类型的值作为参数,添加到目标数组尾部
[1, 2, 3].concat(4, 5, 6)
// [1, 2, 3, 4, 5, 6]
// 如果数组成员包括对象,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用
var obj = { a: 1 };
var oldArray = [obj];
var newArray = oldArray.concat();
obj.a = 2;
newArray[0].a // 2
上面代码中,原数组包含一个对象,concat方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变。
- Array.reverse()
reverse
方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组。
var a = ['a', 'b', 'c'];
a.reverse() // ["c", "b", "a"]
a // ["c", "b", "a"]
- Array.slice()
slice
方法用于提取目标数组的一部分,返回一个新数组,原数组不变。
arr.slice(start, end);
第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员
var a = ['a', 'b', 'c'];
a.slice(0) // ["a", "b", "c"]
a.slice(1) // ["b", "c"]
a.slice(1, 2) // ["b"]
a.slice(2, 6) // ["c"]
a.slice() // ["a", "b", "c"] 最后一个例子没有参数,实际上等于返回一个原数组的拷贝
// 如果slice方法的参数是负数,则表示倒数计算的位置
var a = ['a', 'b', 'c'];
a.slice(-2) // ["b", "c"]
a.slice(-2, -1) // ["b"] -2表示倒数计算的第二个位置,-1表示倒数计算的第一个位置
// 如果第一个参数大于等于数组长度,或者第二个参数小于第一个参数,则返回空数组
var a = ['a', 'b', 'c'];
a.slice(4) // []
a.slice(2, 1) // []
slice
方法的一个重要应用,是将类似数组的对象转为真正的数组
Array.prototype.slice.call({
0: 'a',
1: 'b',
length: 2
})
// ['a', 'b']
Array.prototype.slice.call(document.querySelectorAll("div"));
Array.prototype.slice.call(arguments);
上面代码的参数都不是数组,但是通过call
方法,在它们上面调用slice
方法,就可以把它们转为真正的数组。
- Array.splice()
splice
方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组。
// 第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多的参数,则表示这些就是要被插入数组的新元素
arr.splice(start, count, addElement1, addElement2, ...);
// 从原数组4号位置,删除了两个数组成员
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2) // ["e", "f"]
a // ["a", "b", "c", "d"]
// 除了删除成员,还插入了两个新成员
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(4, 2, 1, 2) // ["e", "f"]
a // ["a", "b", "c", "d", 1, 2]
// 起始位置如果是负数,就表示从倒数位置开始删除
var a = ['a', 'b', 'c', 'd', 'e', 'f'];
a.splice(-4, 2) // ["c", "d"] 倒数第四个位置c开始删除两个成员
// 如果只是单纯地插入元素,splice方法的第二个参数可以设为0。
var a = [1, 1, 1];
a.splice(1, 0, 2) // []
a // [1, 2, 1, 1]
// 如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组。
var a = [1, 2, 3, 4];
a.splice(2) // [3, 4]
a // [1, 2]
- Array.sort()
sort
方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变。
['d', 'c', 'b', 'a'].sort()
// ['a', 'b', 'c', 'd']
[4, 3, 2, 1].sort()
// [1, 2, 3, 4]
[11, 101].sort()
// [101, 11]
[10111, 1101, 111].sort()
// [10111, 1101, 111]
上面代码的最后两个例子,需要特殊注意。sort
方法不是按照大小排序,而是按照字典顺序。也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。
// 如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数
[10111, 1101, 111].sort(function (a, b) {
return a - b;
})
// [111, 1101, 10111]
上面代码中,sort
的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0
,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面。
[
{ name: "张三", age: 30 },
{ name: "李四", age: 24 },
{ name: "王五", age: 28 }
].sort(function (o1, o2) {
return o1.age - o2.age;
})
// [
// { name: "李四", age: 24 },
// { name: "王五", age: 28 },
// { name: "张三", age: 30 }
// ]
- Array.map()
map
方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回。
var numbers = [1, 2, 3];
numbers.map(function (n) {
return n + 1;
});
// [2, 3, 4]
numbers
// [1, 2, 3]
上面代码中,numbers
数组的所有成员依次执行参数函数,运行结果组成一个新数组返回,原数组没有变化。
map
方法接受一个函数作为参数。该函数调用时,map
方法向它传入三个参数:当前成员、当前位置和数组本身。
[1, 2, 3].map(function(elem, index, arr) {
return elem * index;
});
// [0, 2, 6]
上面代码中,map
方法的回调函数有三个参数,elem
为当前成员的值,index
为当前成员的位置,arr
为原数组([1, 2, 3]
)。
map
方法还可以接受第二个参数,用来绑定回调函数内部的this
变量。
var arr = ['a', 'b', 'c'];
[1, 2].map(function (e) {
return this[e];
}, arr)
// ['b', 'c']
上面代码通过map
方法的第二个参数,将回调函数内部的this
对象,指向arr
数组。
如果数组有空位,map
方法的回调函数在这个位置不会执行,会跳过数组的空位。
var f = function (n) { return 'a' };
[1, undefined, 2].map(f) // ["a", "a", "a"]
[1, null, 2].map(f) // ["a", "a", "a"]
[1, , 2].map(f) // ["a", , "a"]
上面代码中,map
方法不会跳过undefined
和null
,但是会跳过空位。
- Array.forEach()
forEach
方法与map
方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach
方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用map
方法,否则使用forEach
方法。
forEach
的用法与map
方法一致,参数是一个函数,该函数同样接受三个参数:当前值、当前位置、整个数组。
function log(element, index, array) {
console.log('[' + index + '] = ' + element);
}
[2, 5, 9].forEach(log);
// [0] = 2
// [1] = 5
// [2] = 9
上面代码中,forEach
遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用map
方法。
forEach
方法也可以接受第二个参数,绑定参数函数的this变量。
var out = [];
[1, 2, 3].forEach(function(elem) {
this.push(elem * elem);
}, out);
out // [1, 4, 9]
上面代码中,空数组out
是forEach
方法的第二个参数,结果,回调函数内部的this
关键字就指向out
。
注意,forEach
方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时,就中断遍历,要使用for
循环。
var arr = [1, 2, 3];
for (var i = 0; i < arr.length; i++) {
if (arr[i] === 2) break;
console.log(arr[i]);
}
// 1
上面代码中,执行到数组的第二个成员时,就会中断执行。forEach
方法做不到这一点。
forEach
方法也会跳过数组的空位。
var log = function (n) {
console.log(n + 1);
};
[1, undefined, 2].forEach(log)
// 2
// NaN
// 3
[1, null, 2].forEach(log)
// 2
// 1
// 3
[1, , 2].forEach(log)
// 2
// 3
上面代码中,forEach
方法不会跳过undefined
和null
,但会跳过空位。
- Array.filter()
filter
方法用于过滤数组成员,满足条件的成员组成一个新数组返回。
它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true
的成员组成一个新数组返回。该方法不会改变原数组。
[1, 2, 3, 4, 5].filter(function (elem) {
return (elem > 3);
})
// [4, 5] 上面代码将大于3的数组成员,作为一个新数组返回。
var arr = [0, 1, 'a', false];
arr.filter(Boolean)
// [1, "a"] 上面代码中,filter方法返回数组arr里面所有布尔值为true的成员。
filter
方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组
[1, 2, 3, 4, 5].filter(function (elem, index, arr) {
return index % 2 === 0;
});
// [1, 3, 5] 上面代码返回偶数位置的成员组成的新数组。
filter
方法还可以接受第二个参数,用来绑定参数函数内部的this
变量。
var obj = { MAX: 3 };
var myFilter = function (item) {
if (item > this.MAX) return true;
};
var arr = [2, 8, 3, 4, 1, 3, 2, 9];
arr.filter(myFilter, obj) // [8, 4, 9]
上面代码中,过滤器myFilter
内部有this变量,它可以被filter
方法的第二个参数obj
绑定,返回大于3
的成员。
- Array.some(),Array.every()
这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。
它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。
some
方法是只要一个成员的返回值是true
,则整个some
方法的返回值就是true
,否则返回false
。(一真即真)
var arr = [1, 2, 3, 4, 5];
arr.some(function (elem, index, arr) {
return elem >= 3;
});
// true 上面代码中,如果数组arr有一个成员大于等于3,some方法就返回true。
every
方法是所有成员的返回值都是true
,整个every
方法才返回true
,否则返回false
。(一假即假)
var arr = [1, 2, 3, 4, 5];
arr.every(function (elem, index, arr) {
return elem >= 3;
});
// false 上面代码中,数组arr并非所有成员大于等于3,所以返回false。
注意,对于空数组,some
方法返回false
,every
方法返回true
,回调函数都不会执行。
function isEven(x) { return x % 2 === 0 }
[].some(isEven) // false
[].every(isEven) // true
some
和every
方法还可以接受第二个参数,用来绑定参数函数内部的`this变量。
- Array.reduce(),Array.reduceRight()
reduce
方法和reduceRight
方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce
是从左到右处理(从第一个成员到最后一个成员),reduceRight
则是从右到左(从最后一个成员到第一个成员),其他完全一样。
[1, 2, 3, 4, 5].reduce(function (a, b) {
console.log(a, b);
return a + b;
})
// 1 2
// 3 3
// 6 4
// 10 5
//最后结果:15
上面代码中,reduce
方法求出数组所有成员的和。第一次执行,a是数组的第一个成员1,b是数组的第二个成员2。第二次执行,a为上一轮的返回值3,b为第三个成员3。第三次执行,a为上一轮的返回值6,b为第四个成员4。第四次执行,a为上一轮返回值10,b为第五个成员5。至此所有成员遍历完成,整个方法的返回值就是最后一轮的返回值15。
reduce
方法和reduceRight
方法的第一个参数都是一个函数。该函数接受以下四个参数。
1.累积变量,默认为数组的第一个成员
2.当前变量,默认为数组的第二个成员
3.当前位置(从0开始)
4.原数组
这四个参数之中,只有前两个是必须的,后两个则是可选的。
如果要对累积变量指定初值,可以把它放在reduce
方法和reduceRight
方法的第二个参数。
[1, 2, 3, 4, 5].reduce(function (a, b) {
return a + b;
}, 10);
// 25
上面代码指定参数a的初值为10,所以数组从10开始累加,最终结果为25。注意,这时b是从数组的第一个成员开始遍历。
上面的第二个参数相当于设定了默认值,处理空数组时尤其有用。
function add(prev, cur) {
return prev + cur;
}
[].reduce(add)
// TypeError: Reduce of empty array with no initial value
[].reduce(add, 1)
// 1
上面代码中,由于空数组取不到初始值,reduce
方法会报错。这时,加上第二个参数,就能保证总是会返回一个值。
下面是一个reduceRight
方法的例子。
function substract(prev, cur) {
return prev - cur;
}
[3, 2, 1].reduce(substract) // 0
[3, 2, 1].reduceRight(substract) // -4
上面代码中,reduce
方法相当于3减去2再减去1,reduceRight
方法相当于1减去2再减去3。
由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员。
function findLongest(entries) {
return entries.reduce(function (longest, entry) {
return entry.length > longest.length ? entry : longest;
}, '');
}
findLongest(['aaa', 'bb', 'c']) // "aaa"
上面代码中,reduce
的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员。
- indexOf(),lastIndexOf()
indexOf
方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1
。
var a = ['a', 'b', 'c'];
a.indexOf('b') // 1
a.indexOf('y') // -1
indexOf
方法还可以接受第二个参数,表示搜索的开始位置。
['a', 'b', 'c'].indexOf('a', 1) // -1
// 上面代码从1号位置开始搜索字符a,结果为`-1`,表示没有搜索到。
lastIndexOf
方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1
。
var a = [2, 5, 9, 2];
a.lastIndexOf(2) // 3
a.lastIndexOf(7) // -1
注意,这两个方法不能用来搜索NaN
的位置,即它们无法确定数组成员是否包含NaN
。
[NaN].indexOf(NaN) // -1
[NaN].lastIndexOf(NaN) // -1
这是因为这两个方法内部,使用严格相等运算符(===)进行比较,而NaN
是唯一一个不等于自身的值。
- 链式使用
上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用。
var users = [
{name: 'tom', email: '[email protected]'},
{name: 'peter', email: '[email protected]'}
];
users
.map(function (user) {
return user.email;
})
.filter(function (email) {
return /^t/.test(email);
})
.forEach(console.log);
// "[email protected]"
上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以t开头的 Email 地址。
22. 数组的扁平化处理(降维)
- 数组的扁平化处理, 将多层的数组转成一维数组,例如将
[1, [2], [[3]]] => [1,2,3]
1.使用Array.prototype.flat(depth)。depth不能小于数组的深度
arr.flat(3)
2.遍历
function flat1(arr) {
while(arr.some(item=>Array.isArray(item))) {
arr = [].concat(...arr);
}
return arr;
}
3.递归实现
function flat2(arr1) {
return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flat2(val)) : acc.concat(val), []);
}
4.非递归
function stackFlatten(input) {
const stack = [...input];
const res = [];
while (stack.length) {
const next = stack.pop();
if (Array.isArray(next)) {
stack.push(...next);
} else {
res.push(next);
}
}
return res.reverse();
}
- 对象的扁平化,只包含普通类型,数组和对象。
{
num: 1,
arr: [1, 2]
obj: {
name: 'name'
}
}
// 偏平化后=>
{
num: 1,
arr.[0]: 1,
arr.[1]: 2,
obj.name: 'name'
}
23. 深拷贝浅拷贝
首先,浅拷贝和深拷贝都只针对于像Object, Array这样的复杂对象。
区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制
深拷贝:
深拷贝就是增加一个指针,并申请一个新的内存,并且让这个新增加的指针指向这个新的内存地址,使用深拷贝,在释放内存的时候就不会像浅拷贝一样出现重复释放同一段内存的错误,当我们需要复制原对象而又不能修改元对象的时候,深拷贝就是一个,也是唯一的选择。
比如说:b通过a分享的链接买了一件和a一样的衣服,但是a和b的衣服却是两件长得一样的衣服,不管a的衣服破了脏了都不影响b的衣服。
1.通过利用JSON.parse(JSON.stringify(Object))
来达到深拷贝的目的
2.但是JSON深拷贝的缺点是undefined和function还有symbol类型是无法进行深拷贝的
3.如有需要可以自己手动封装函数来达到目的
- 浅拷贝:
简单的来说,浅拷贝就是增加了一个指针指向已存在的内存(JavaScript并没有指针的概念,这里只是用于辅助说明),浅拷贝只是拷贝了内存的地址,子类的属性指向的是父类属性的内存地址,当子类的属性修改后,父类的属性也会随之被修改。
比如说:一件衣服两个人穿,不管你穿还是我穿都还是那件衣服。
1.通过ES6新特性Object.assign({}, target) 或 {...target}
来达到浅拷贝的目的
2.优点是:可以解决JSON不能处理或是无法拷贝的问题
3.缺点是:只能深拷贝最顶上的一层,不能拷贝原型链上的属性
应用场景
1、从服务器fetch到数据之后我将其存放在store中,通过props传递给界面,然后我需要对这堆数据进行修改,那涉及到修改就一定有保存和取消,所以我们需要将这堆数据拷贝到其他地方(网友的经历)
2、当你想使用某个对象的值,在修改时不想修改原对象,那么可以用深拷贝来弄一个新的内存对象。像es6的新增方法都是深拷贝,所以推荐使用es6语法。
3、es5数组新增的map,filter等方法都是深拷贝,日常开发很常用,比如重组格式会使用map,map在react中是非常常用的,filter则是过滤返回一个新的数组。解构赋值和深拷贝、浅拷贝
24. 防抖和节流
防抖和节流是针对响应跟不上触发频率这类问题的两种解决方案。
函数防抖: debounce
定义:多次触发事件后,事件处理函数只执行一次,并且是在触发操作结束时执行。
function debounce(fn, delay) {
let timer;
return function(...rest) {
timer && clearTimeout(timer)
timer = setTimeout(() => fn.apply(this, rest), delay)
}
}
- 函数节流: throttle
1.定义:触发函数事件后,短时间间隔内无法连续调用,只有上一次函数执行后,过了规定的时间间隔,才能进行下一次的函数调用。
2.通过时间戳或者定时器的方式实现节流。
function throttle(fn, delay) {
let start
return function(...rest) {
let now = Date.now()
!start && (start = now)
if (now - start >= delay) {
fn.apply(this, rest)
start = now
}
}
}
function throttle2(fn, delay){
let timer
return function(...rest){
if(!timer){
timer = setTimeout(() => {
fn.apply(this, rest);
timer = null;
}, delay)
}
}
}
25. 原生ajax
创建xhr实例
open链接(请求方法,url, 同步异步)
设置请求参数
监听onreadystatechange事件
发送
var xhr=new XMLHttpRequest();
xhr.open('POST',url,false);
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.onreadystatechange=function(){
// readyState == 4说明请求已完成
if(xhr.readyState==4){
if(xhr.status==200 || xhr.status==304){
console.log(xhr.responseText);
fn.call(xhr.responseText);
}
}
}
xhr.send();
26. 函数柯里化
- 柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。
function curry(fn, ...args) {
return args.length < fn.length ? (...arguments) => curry(fn, ...args, ...arguments) : fn(...args)
}
- 特点:
1.延迟参数传递,参数复用
2.代码短小,优雅,函数化,有点不好理解
27. map和forEach的区别
都支持三个参数,参数分别为
item
(当前每一项),index
(索引值),arr
(原数组)forEach
允许callback
更改原始数组的元素,无返回值。map
则返回新的数组,表现上可认为是浅拷贝后进行操作。forEach,filter,every,some
会跳过空位,map
会跳过空位,但是会保留这个值。
28. Cookie和Session的区别
通俗讲,Cookie是访问某些网站以后在本地存储的一些网站相关的信息,下次再访问的时候减少一些步骤。另外一个更准确的说法是:Cookies是服务器在本地机器上存储的小段文本并随每一个请求发送至同一个服务器,是一种在客户端保持状态的方案。
Session是存在服务器的一种用来存放用户数据的类HashTable结构。
区别
1.通过上面的简单叙述,很容易看出来最明显的不同是一个在客户端一个在服务端。因为Cookie存在客户端所以用户可以看见,所以也可以编辑伪造,不是十分安全。
2.Session过多的时候会消耗服务器资源,所以大型网站会有专门的Session服务器,而Cookie存在客户端所以没什么问题。
3.域的支持范围不一样,比方说a.com
的Cookie在a.com
下都能用,而www.a.com
的Session在api.a.com
下都不能用,解决这个问题的办法是JSONP或者跨域资源共享
29. 事件捕获流,冒泡流和事件委托
事件流描述的是从页面中接收事件的顺序。
类型
1.事件冒泡流:事件的传播是从最特定的事件目标到最不特定的事件目标。即从DOM树的叶子到根。(IE)
2.事件捕获流:事件的传播是从最不特定的事件目标到最特定的事件目标。即从DOM树的根到叶子。(网景公司)
DOM标准规定事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。
事件捕获阶段:实际目标(
)在捕获阶段不会接收事件。也就是在捕获阶段,事件从window到document再到body就停止了。 处于目标阶段:事件在
上发生并处理。但是事件处理会被看成是冒泡阶段的一部分。 冒泡阶段:事件又传播回文档。
事件委托又叫事件代理,是根据事件冒泡流,让父元素代理响应函数减少DOM的访问
注意以下事件不支持冒泡:
1.blur
2.focus
3.load
4.unload
5.以及自定义的事件。
6.原因是在于:这些事件仅发生于自身上,而它的任何父节点上的事件都不会产生,所有不会冒泡
- 如何阻止事件捕获或冒泡
//阻止冒泡
e.stopPropagation() || return false。
window.e.cancelBubble=true; // IE
// 阻止捕获
e.stopImmediatePropagation() // 阻止捕获和其他事件
// 阻止默认事件: 事件处理过程中,不阻击事件冒泡,但阻击默认行为
e.preventDefault()
window.e.returnValue=false; || return false;// IE
30. 作用域、作用域链
某个变量有( 起 ) 作用的范围
块级作用域, 在别的语言里有块级作用域, 但是在js中没有块级作用域
js
中的作用域
1.script
构成了全局作用域
2.在js
中函数是唯一一个可以创建作用域的对象
- 词法作用域 - 动态作用域
1.词法作用域:在变量声明的时候,它的作用域就已经确定了
2.动态作用域:在程序运行的时候,由程序的当前上下文(执行环境)决定的
3.js
属于词法作用域
- 词法作用域的访问规则:
先在当前作用域中查找,如果找到就直接使用,如果没有找到,那么就到上一级作用域中查找,如果还没有找到那么就重复这个查找的过程,直到全局作用域
- 作用域链
1.js
中函数可以创建作用域
2.js
中的函数中可以声明函数
3.函数内部的函数中又可以声明函数
4.以上,会形成一个链式的结构,这个是作用域链
31. 数组去重
关键考你是用内存空间换时间呢,还是用时间来换内存空间呢?
这句话是什么意思呢?意思就是用数组去重的方法不是单一的,有很多种去重的办法,但是这些方法之中呢,有几种方法是特别耗费性能的,而且去重的时间计算,如果你对每种去重方法的时间做一个比较的话,就会发现有的特别快,有的特别慢。特别快的方法,肯定是用内存换来的时间上的提升,而特别慢的方法呢,肯定是用时间换来的空间上的提升。你这么去权衡这个利弊呢?
其中第1种和第3种方法都用到了数组的indexOf方法。此方法的目的是寻找存入参数在数组中第一次出现的位置。很显然,js引擎在实现这个方法的时候会遍历数组直到找到目标为止。所以此函数会浪费掉很多时间。
而第2中方法用的是hash表。把已经出现过的通过下标的形式存入一个object内。下标的引用要比用indexOf搜索数组快的多。
为了判断这三种方法的效率如何,我做了一个测试程序,生成一个10000长度的随机数组来去重测试执行时间。 结果表明第二种方法远远快于其他两种方法。 但是内存占用方面应该第二种方法比较多,因为多了一个hash表。**这就是所谓的空间换时间。 **
- 第四种方法
Array.prototype.only4= function()
{
this.sort();
var re=[this[0]];
for(var i = 1; i < this.length; i++)
{
if( this[i] !== re[re.length-1])
{
re.push(this[i]);
}
}
return re;
}
console.log(arr.only3());
这个方法的思路是先把数组排序,然后比较相邻的两个值。 排序的时候用的JS原生的sort方法,JS引擎内部应该是用的快速排序吧。 最终测试的结果是此方法运行时间平均是第二种方法的三倍左右,不过比第一种和第三种方法快了不少。
然而当你提到排序的时候,面试官就会继续追问你,除了sort()
,你还会那种排序。显然这是一个无底洞,当你回答上来桶排序,冒泡排序,希尔排序的时候,他会继续问你,你对那种比较熟悉,现在能写的出来么?
下面是十大排序
冒泡排序最为稳定 但是几乎没有什么卵用
1. 钩子函数和回调函数的区别
很明显的差别就是:钩子函数在捕获消息的第一时间就执行,而回调函数是捕获结束时,最后一个被执行的。
回调函数其实是调用者将回调函数的指针传递给了调用函数,当调用函数执行完毕后,通过函数指针来调用回调函数。而钩子函数在消息刚发出,没到达目的窗口前就先捕获了该消息,先得到控制权执行钩子函数,所以他可以加工改变该消息,当然也可以不作为,还可以强行结束该消息。
2. MVC模式
所有通信都是单向的。
MVC
模式的意思是,软件可以分成三个部分
1.视图(View
):用户界面。
2.控制器(Controller
):业务逻辑
3.模型(Model
):数据保存
- 各部分之间的通信方式如下
1.View
传送指令到 Controller
2.Controller
完成业务逻辑后,要求 Model
改变状态
3.Model
将新的数据发送到 View
,用户得到反馈
- 互动模式 - 接受用户指令时,
MVC
可以分成两种方式
1.一种是通过 View
接受指令,传递给 Controller
。
2.另外一种是直接通过Controller
接受指令。
- 实例:Backbone 实际项目往往采用更灵活的方式
1.用户可以向 View
发送指令(DOM
事件),再由 View
直接要求 Model
改变状态。
2.用户也可以直接向 Controller
发送指令(改变 URL
触发 hashChange
事件),再由 Controller
发送给 View
。
3.Controller
非常薄,只起到路由的作用,而 View
非常厚,业务逻辑都部署在 View
。所以,Backbone
索性取消了 Controller
,只保留一个 Router
(路由器) 。
3. MVP模式
-
MVP
模式将Controller
改名为Presenter
,同时改变了通信方向。
1.各部分之间的通信,都是双向的。
2.View
与 Model
不发生联系,都通过 Presenter
传递。
3.View
非常薄,不部署任何业务逻辑,称为"被动视图"(Passive View
),即没有任何主动性,而 Presenter
非常厚,所有逻辑都部署在那里。
4. MVVM模式
MVVM
模式将Presenter
改名为ViewModel
,基本上与MVP
模式完全一致。唯一的区别是,它采用双向绑定(
data-binding
):View
的变动,自动反映在ViewModel
,反之亦然。相比前两种模式,更加的高效,简洁,操作性和复用性也搞
5. 关于MVVM的优缺点
优点
方便测试
在MVC
下,Controller
基本是无法测试的,里面混杂了个各种逻辑,而且分散在不同的地方。有了MVVM
我们就可以测试里面的ViewModel
,来验证我们的处理结果对不对(Xcode7的测试已经越来越完善了)。
- 便于代码的移植
比如iOS
里面有iPhone
版本和iPad
版本,除了交互展示不一样外,业务逻辑的Model
是一致的。这样,我们就可以以很小的代价去开发另一个App
。(以前做公司iPad
的时候就深深感觉到,全部在VC
里面是多么的痛苦和重新开发一个没有啥区别)。
- 兼容
MVC
MVVM
是MVC
的一个升级版,目前的MVC
也可以很快的转换到MVVM这个模式。VC
可以省去一大部分展示逻辑。
- 缺点
类会增多,每个VC
都附带一个ViewModel
,类的数量*2
-
ViewModel
会越来越庞大
我们把逻辑给了ViewModel
,那势必Model
也会变得很复杂,里面的属性和方法越来越多。可能重写的方法比较多,因为涉及到一些数据的转换以及和Controller
之间的通信。
- 调用复杂度增加
由于数据都是从ViewModel
来,想想突然来了一个新人,一看代码,不知道真实的模型是谁。比如常用Tableview
的数据源,一般都是一个数组,如果不断的通过ViewModel
去取,沟通上没有那么直接。况且每封一层,意味着要写很多代码去融合他们的转换。
6. 如何理解大前端时代
简单来说,大前端就是所有前端的统称,比如Android、iOS、web、Watch等,最接近用户的那一层也就是UI层,然后将其统一起来,就是大前端。大前端最大的特点在于一次开发,同时适用于所有平台,开发者不用为一个APP需要做Android和iOS两种模式而担心。大前端是web统一的时代,利用web不仅能开发出网站,更可以开发手机端web应用和移动端应用程序。
由于node的出现,前端工程师不需要依赖于后端程序而直接运行,从而前后端分离起来。所以当开发一个新产品的时候服务只需要写一次,但是面向用户的产品可能有很多,例如网站、Android客户端、iOS客户端和微信小程序等。由于各个平台使用的技术栈都不一样,代码无法复用,非常浪费人力、物力。那么有没有什么技术能够解决这一痛点呢?大前端应运而生,其实大前端的主要核心就是跨平台技术,有了跨平台技术,各个平台的差异性就抹平了,开发者只需要一套技术栈就可以开发出适用于多个平台的客户端。
目前的主流跨平台方案:Cordova/phoneGap、React Native、Weex、微信小程序、PWA和Flutter等,根据其原理性,可以分为三大类。
1.H5+原生(Cordova、Ionic、微信小程序)
2.JavaScript开发+原生渲染 (React Native、Weex、快应用)
3.自绘UI+原生(Flutter)
4.增强版Web App(PWA)
5.接下来简单介绍这三种跨平台方案。
- H5+原生混合开发
这种模式又称为Hybrid开发,现在很多App都用这种模式去开发,常见的有微信、淘宝、美团、爱奇艺等知名移动应用等。国内也有很多公司使用Hybrid模式去开发平台,供开发者使用,像Dcloud、AppCan、Inoic等,基本上都是参考Cordova衍生出的混合开发框架。
JavaScript开发+原生渲染
这类开源框架的代表主要是Facebook的React Native、阿里的Weex,当然也有未开源的美团的Picasso,以及最新推出的快应用。
JavaScript开发+原生渲染的方式主要优点如下:
1.采用Web开发技术栈,社区庞大、上手快、开发成本相对较低。
2.原生渲染,性能相比H5提高很多。
3.动态化较好,支持热更新。
- 当然也有缺点如下:
1.渲染时需要JavaScript和原生之间通信,在有些场景如拖动可能会因为通信频繁导致卡顿。
2.JavaScript为脚本语言,执行时需要JIT,执行效率和AOT代码仍有差距。
3.由于渲染依赖原生控件,不同平台的控件需要单独维护,并且当系统更新时,社区控件可能会滞后;除此之外,其控件系统也会受到原生UI系统限制,例如,在Android中,手势冲突消歧规则是固定的,这在使用不同人写的控件嵌套时,手势冲突问题将会变得非常棘手。
7. React Native
React Native (简称RN)是Facebook于2015年4月开源的跨平台移动应用开发框架,是Facebook早先开源的JS框架 React 在原生移动应用平台的衍生产物,目前支持iOS和Android两个平台。RN使用Javascript语言,类似于HTML的JSX,以及CSS来开发移动应用,因此熟悉Web前端开发的技术人员只需很少的学习就可以进入移动应用开发领域。
React Native的原理和React设计一致,React中虚拟DOM最终会映射为浏览器DOM树,而RN中虚拟DOM会通过 JavaScriptCore 映射为原生控件树。
JavaScriptCore 是一个JavaScript解释器,它在React Native中主要有两个作用:
1.为JavaScript提供运行环境。
2.是JavaScript与原生应用之间通信的桥梁,作用和JsBridge一样,事实上,在iOS中,很多3.JsBridge的实现都是基于 JavaScriptCore 。
- 而RN中将虚拟DOM映射为原生控件的过程中分两步:
1.布局消息传递; 将虚拟DOM布局信息传递给原生
2.原生根据布局信息通过对应的原生控件渲染控件树
3.由于React Native是原生控件渲染,所以性能会比混合应用中H5好很多,同时React Native是Web开发技术栈,只需维护一份代码,即可在多个平台上使用。
8. TypeScript
TypeScript是 JavaScript 的一个超集,主要提供了类型系统和对 ES6 的支持,它由 Microsoft 开发。
TypeScript 是 JavaScript 的类型的超集,它可以编译成纯 JavaScript。编译出来的 JavaScript 可以运行在任何浏览器上。TypeScript 编译工具可以运行在任何服务器和任何系统上。TypeScript 是开源的。
TypeScript 增加了代码的可读性和可维护性
1.类型系统实际上是最好的文档,大部分的函数看看类型的定义就可以知道如何使用了
2.可以在编译阶段就发现大部分错误,这总比在运行时候出错好
3.增强了编辑器和 IDE 的功能,包括代码补全、接口提示、跳转到定义、重构等
- TypeScript 非常包容
1.TypeScript 是 JavaScript 的超集,.js 文件可以直接重命名为 .ts 即可
2.即使不显式的定义类型,也能够自动做出类型推论
3.可以定义从简单到复杂的几乎一切类型
4.即使 TypeScript 编译报错,也可以生成 JavaScript 文件
5.兼容第三方库,即使第三方库不是用 TypeScript 写的,也可以编写单独的类型文件供 TypeScript 读取
- TypeScript 拥有活跃的社区
1.大部分第三方库都有提供给 TypeScript 的类型定义文件。
2.Google 开发的 Angular2 就是使用 TypeScript 编写的。
3.TypeScript 拥抱了 ES6 规范,也支持部分 ESNext 草案的规范。
- TypeScript 的缺点
1.有一定的学习成本,需要理解接口(Interfaces)、泛型(Generics)、类(Classes)、枚举类型(Enums)等前端工程师可能不是很熟悉的概念。
2.短期可能会增加一些开发成本,毕竟要多写一些类型的定义,不过对于一个需要长期维护的项目,TypeScript 能够减少其维护成本。
3.集成到构建流程需要一些工作量。
4.可能和一些库结合的不是很完美。