1. ES5和ES6的继承
1.1: es5的继承:
原型链继承
将父类的
实例
作为子类的原型链指向,继承的属性和方法都在子类原型上 缺点:(所有子类实例的原型共用同一个父类的实例,子类实例可以通过this[引用类型].push()修改父类实例的属性,造成污染)
- 继承父类的属性如果是引用类型,则多个子类实例共享该引用类型数据,操作会被篡改。
- 创建子类实例时,无法初始化继承的属性
function Father(father) {
this.father = father;
}
Father.prototype.fatherFn = funciton() {
console.log(this.father)
}
function Child(child) {
this.child = child;
}
// 继承
Child.prototype = new Father();
Child.prototype.childFn = funciton() {
console.log(this.child);
}
构造函数继承
在子类构造函数中使用父类的构造函数:Fater.call(this),修改子类实例this的属性和方法
function Father(father) {
this.father = father;
}
Father.prototype.fatherFn = funciton() {
console.log(this.father)
}
function Child(child) {
this.child = child;
// 继承
Father.call(this);
}
Child.prototype.childFn = funciton() {
console.log(this.child);
}
缺点:
- 只能继承父类的实例属性和方法,不能继承原型属性/方法
- 每个子类实例都有父类实例函数的副本,影响性能
组合继承
使用父类的构造函数继承父类实例的属性和方法,使用修改原型链继承父类原型的属性和方法
function Father(father) {
this.father = father;
}
Father.prototype.fatherFn = funciton() {
console.log(this.father)
}
function Child(child) {
this.child = child;
// 继承
// 第二次调用Father()
Father.call(this);
}
// 第一次调用Father()
Child.prototype = new Father();
Child.prototype.constructor = Child // 重写Child.prototype的constructor属性,指向自己的构造函数
Child.prototype.childFn = funciton() {
console.log(this.child);
}
缺点:
- 会调用两次父类的构造函数,在使用子类创建实例对象时,其原型中会存在两份相同的属性/方法。
寄生组合继承
使用Object.create(Father.prototype)修改子类的原型
避免了在Child.prototype
上创建不必要的、多余的属性
function Father(father) {
this.father = father;
}
Father.prototype.fatherFn = funciton() {
console.log(this.father)
}
function Child(child, father) {
this.child = child;
// 继承
// 只在此处调用Father()
Father.call(this,father);
}
// 此处与组合继承的区别,直接将Father的原型加入到Child的原型链中
Child.prototype = Object.create(Father.prototype);
Child.prototype.constructor = Child // 重写Child.prototype的constructor属性,指向自己的构造函数
Child.prototype.childFn = funciton() {
console.log(this.child);
}
1.2: es6类的继承extend
es6的继承使用extend,其实就是寄生组合式继承方式的语法糖。
- 在子类的constructor中(没有this对象),必须先使用super创建父类的实例,然后用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
class Rectangle {
// constructor
constructor(height, width) {
this.height = height;
this.width = width;
}
// Getter
get area() {
return this.calcArea()
}
// Method
calcArea() {
return this.height * this.width;
}
}
const rectangle = new Rectangle(10, 20);
console.log(rectangle.area);
// 输出 200
-----------------------------------------------------------------
// 继承
class Square extends Rectangle {
constructor(length) {
super(length, length);
// 如果子类中存在构造函数,则需要在使用“this”之前首先调用 super()。
this.name = 'Square';
}
get area() {
return this.height * this.width;
}
}
const square = new Square(10);
console.log(square.area);
// 输出 100
1.3: 5和6的继承区别:
1、函数声明和类声明的区别
函数声明会提升,类声明不会。首先需要声明你的类,然后访问它,否则像下面的代码会抛出一个ReferenceError。
let p = new Rectangle();
// ReferenceError
class Rectangle {}
2、ES5继承和ES6继承的区别
- ES5的继承实质上是先创建子类的实例对象,然后再将父类的方法添加到this上(Parent.call(this)).
- ES6的继承有所不同,实质上是先创建父类的实例对象this,然后再用子类的构造函数修改this。因为子类没有自己的this对象,所以必须先调用父类的super()方法,否则新建实例报错。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y) // 调用父类的constructor(x, y), super表示父类的构造函数
this.color = color;
}
}
let cp = new ColorPoint();
2.前端路由的实现
一共有两种模式,hash 和 history(1.hash值可以用<a 标签和location.href修改当前触发页面地址,触发hashchange事件,不会发请求。2.可以在点击事件里手动触发pushState方法,也不会发请求)
2.1: hash模式: location.hash+hashchange事件
后面 hash 值的变化,并不会导致浏览器向服务器发出请求,浏览器不发出请求,也就不会刷新页面
http://www.xxx.com/#/login
function matchAndUpdate () { // todo 匹配 hash 做 dom 更新操作 }
window.addEventListener('hashchange', matchAndUpdate)
2.2:history模式: history.pushState()+popState事件
history.length // 表示当前窗口访问过的所有页面网址
history.state:History 堆栈最上层的状态值
// 后退到前一个网址
history.back()
// 等同于
history.go(-1)
// 前进到下个网址
history.forward()
两个方法:History.pushState(state, title, url) 和 History.replaceState(state, title, url)都不会触发页面刷新,只是导致history对象和地址栏变化。
state
:一个与添加的记录相关联的状态对象,主要用于popstate
事件。该事件触发时,该对象会传入回调函数。也就是说,浏览器会将这个对象序列化以后保留在本地,重新载入这个页面的时候,可以拿到这个对象。如果不需要这个对象,此处可以填null
。title
:新页面的标题。但是,现在所有浏览器都忽视这个参数,所以这里可以填空字符串。url
:新的网址,必须与当前页面处在同一个域。浏览器的地址栏将显示这个网址。
var stateObj = { foo: 'bar' };
history.pushState(stateObj, 'page 2', '2.html');
history.state // {foo: "bar"}
// replaceState只是修改最顶层的state,不会新增
history.pushState({page: 1}, 'title 1', '?page=1')
// URL 显示为 http://example.com/example.html?page=1
history.pushState({page: 2}, 'title 2', '?page=2');
// URL 显示为 http://example.com/example.html?page=2
history.replaceState({page: 3}, 'title 3', '?page=3');
// URL 显示为 http://example.com/example.html?page=3
history.back()
// URL 显示为 http://example.com/example.html?page=1
popstate事件:
只有用户点击浏览器倒退按钮和前进按钮,或者使用 JavaScript 调用History.back()
、History.forward()
、History.go()
方法时才会触发。(页面第一次加载的时候,浏览器不会触发该事件)
- 需要服务端配合,如果pushState修改了当前页面的url(没有对应的页面),此时刷新时会出现找不到对应的404url,
window.addEventListener('popstate', function(event) {
console.log('location: ' + document.location);
console.log('state: ' + JSON.stringify(event.state));
});
3.字符转number和parseInt的陷阱
parseInt(str, 进制):其中str如果不是字符串,则会执行(str).toString(),进制(有效值:2-36)为0,Null, undefined则忽略作为10进制转换,如果str从第一位开始就不符合进制规则,则返回NaN,否则只转换符合规则的头部 整数类型的会看已0x开头还是0开头。
- 0开头的数字认为是8进制的表示法
- 0x或0X的数组认为是16进制的表示法
(011).toString() // 011 + '' === '9'
(0x11).toString() // 011 + '' === '17'
parseInt('320', 3); // N
parseInt('023', 3); // '02' === 3
// 例题:
['1','2','3'].map(parseInt) // parseInt(item, index)输出[1, NaN, NaN]
- 字符串转number类型,'0x'或'0X'开头字符串会按照16进制转换,但是字符串以'0'开始并不会按照8进制转换
+'0x10' // parseInt('0x10') === 16
parseInt('011') // +'011' === 11
+011 === 9 // true
+0x11 === 17 // true
011 +'' === '9' // true,相当于(011).toString()
4.commonJS 和 ES Moudle的区别
区别:
- commonJS:
1.require的是返回值(module.exports)的拷贝,多次加载从内存读取。 2.在输入时先加载整个模块,生成一个对象。再从这个对象上读取方法,只有对应子模块加载完成,才能执行后面的操作。(同步/运行时加载,因为模块文件存在硬盘中直接读取)。3.顶层的this指向当前模块
- esMoudle:
1. import的是export的引用,在 2.ES Module不是对象,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。(异步/编译时加载)。3. 顶层的this指向undefined
AMD/CMD是CommonJS在浏览器端的解决方案。CommonJS是同步加载(代码在本地,加载时间基本等于硬盘读取时间)。AMD/CMD是异步加载(浏览器必须这么干,代码在服务端)
循环引用时:由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,在下一次循环中的引用时,不再执行直接读取上次缓存的值
1. commonJS
commonjs规定:每个模块内部,module变量代表当前模块,该变量是一个对象。他有一个exports属性,这个属性是对外的接口。加载某一个模块,其实就是加载该模块的module.exports属性。默认引用.js文件。
commonjs模块的特点:
- 所有代码运行在模块作用域内,不会污染全局变量
- 模块可以加载多次,但是只有第一次加载时运行一次。然后运行结果就被缓存下来,以后再加载,就是直接读取缓存的结果。
// 导出使用module.exports,也可以exports。exports指向module.exports;即exports = module.exports
// 就是在此对象上挂属性
// commonjs
module.exports.add = function add(params) {
return ++params;
}
exports.sub = function sub(params) {
return --params;
}
// 加载模块使用require('xxx')。相对、绝对路径均可。默认引用js,可以不写.js后缀
// index.js
var common = require('./commonjs');
console.log(common.sub(1));
console.log(common.add(1));
2. ES Module(引擎解析时就知道模块的依赖关系)
JS 引擎对脚本静态分析的时候,遇到模块加载命令
import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。同一个模块如果加载多次,将只执行一次。后缀.js不可省略。
// 报错1
export 1;
// 报错2
const m = 1;
export m;
// 接口名与模块内部变量之间,建立了一一对应的关系
// 写法1
export const m = 1;
// 写法2
const m = 1;
export { m };
// 写法3
const m = 1;
export { m as module };
// module.js
// 其实export default就是export { xxx as default }
const m = 1;
export default m;
===
export { m as default }
// index.js
// 对应的输入也得相应变化
import module from './module';
===
import { default as module } from './module';
// 动态加载写法,使用的是import()函数,不是关键字
import('./module').then(({ a }) => {})
// async、await
const { a } = await import('./module');
// 动态加载,错误示例,这些需要js执行才知道具体值是什么,无法在引擎解析时就知道模块的依赖关系
if(true) {
import a from './a.js'
}
const path = './modules/a.js'
import a rom path
// 整体加载
import * as module from './module';
5. js隐式转换的坑
- 字符与数字类型比较:
字符串类型
会被转换为数字类型
(必须是纯数字字符或者空字符串,不然就是NaN)
'123a' == 123 // false,+'123a' 为NaN
"Infinity" == Infinity //true
NaN == NaN //false 因为NaN与任何值都不相等,包括自己
- 布尔类型与其他类型比较:只要
布尔类型
参与比较,该布尔类型
就会率先被转成数字类型
, true转为1,false为0
true == 2 // false
"" == false // true,"" == 0 => 0 == 0
null
类型和undefined
类型与其他类型相比较: 只与子身或者相互宽松相等(==
),与其他所有值都不宽松相等。
null == null //true
undefined == undefined //true
null == undefined //true
null == 0 //false
null == false //false
undefined == 0 //false
undefined == false //false
- 对象与原始类型的相比较
对象
与原始类型
相比较时,会把对象
按照对象转换规则
转换成原始类型
,再比较。
转换过程:1. 有valueOf中toString中的一个,有谁调用谁,2. 如果都有:先调用valueOf,如果返回的不是原始类型则继续调用toString(其中数组的toString,相当于[1, 2, 3].toString() === [1, 2, 3].join(',') === '1,2,3')。比较时自己定义的valueOf中toString如果返回的不是基础类型,则报错:
var o3 = {
valueOf(){ return {} },
toString(){ return 3 }
}
Number(o3) //3
{} == 0 // false
{} == '[object Object]'// true
[] == false // true
[1,2,3] == '1,2,3' // true
// 报错例子
var o2 = {
toString(){
return {a:123}
}
}
o2 == 1 // Uncaught TypeError: Cannot convert object to primitive value
- 对象与对象相比较
如果两个对象指向同一个对象,相等操作符返回
true
,否则为false
。
// !的优先级高于==
[] == ![] // true:先执行Boolean([])为true,则变为[] == !true -> [].toSting() == 0 -> '' == 0
{} == !{} // false: 主要是{}.toString() 为"[object Object]", "[object object]" == 0 -> NaN == 0
-
字符串连接符(+)与加号运算符(+)如何区分
(总结:任意一边为字符串,或对象转原始值后为字符串,是字符串连接,其它情况是加法运算)
以下情况可视为字符串连接符
-
含有
+
两边的数据,任意一个为字符串
"" + 122 // '122'
- 含有
+
两边的数据,其中一边为对象,并且取得的原始值为字符串
1 + {} // '1[object Object]'
以下视为加号运算符
-
加号两边都为数字类型
-
加号两边都是基本类型,除了
字符串类型
,则可视为加号
。也就是说加号两边可以为Boolean
,Number
,null
,undefined
这种情况,肯定是加号运算符。将对不是Numebr
类型的数据执行Number()
方法再相加。NaN
与任何数相加都为NaN
,
1 + true // 2
- 其中一边是基本类型,字符串除外,另一边是对象,并且对象获得的原始值不是字符串。
var c = {
valueOf(){
return {};
}
}
2 + c // 2 + '[object Object]' -> '2[object Object]'
特别的:js解释器
会将开头的 {}
看作一个代码块,而不是一个js对
象,于是运算可写成+[]
[] + {} // "[object object]":"" + "[object object]"
{} + [] // 0: +[] -> +""
{
console.log(1) // 代码块
}
{} + {} // '[object Object][object Object]'
6. cookie的设置和发请求时cookie是怎么携带的?
- cookie设置时:不能设置和访问子域的cookie,可以向当前域或者更高级域设置cookie。
例如
client.com
不能向a.client.com
设置cookie, 而a.client.com
可以向client.com
设置cookie - cookie携带问题:
- fetch方法不管跨不跨域,默认都不携带,需要设置
credentials
,且服务端需要设置响应头Access-Control-Allow-Credentials: true
, 否则浏览器会因为安全限制而报错, 拿不到响应
fetch(url, {
credentials: "include", // include(都带), same-origin(同源下携带), omit(默认,不携带)
})
- axios和jQuery在同域ajax请求时会带上cookie, 跨域请求不会, 跨域请求需要设置
withCredentials
和服务端响应头
axios.get('http://server.com', {withCredentials: true})
7. 一帧的执行
setTimeout(()=>console.log('setTimeout'), 0);
Promise.resolve().then(()=>console.log('promise'));
requestAnimationFrame(()=>console.log('animation'));
requestIdleCallback(()=>console.log('idle'));
// 执行结果大多数情况下是: promise, animation, setTimeout, idle
// 少数情况是:promise, setTimeout, animation, idle
队列特性
- 宏任务队列,每次只会执行队列内的一个任务。
- 微任务队列,每次会执行队列里的全部任务。假设微任务队列内有 100 个 Promise,它们会一次过全部执行完。这种情况下极有可能会导致页面卡顿。如果在微任务执行过程中继续往微任务队列中添加任务,新添加的任务也会在当前事件循环中执行,很容易造成死循环, 如:
function loop() {
Promise.resolve().then(loop);
}
loop();
- RAF 队列,跟微任务队列有点相似,每次会执行队列里的全部任务。但如果在执行过程中往队列中添加新的任务,新的任务不会在当前事件循环中执行,而是在下次事件循环中执行。
- idle 队列,每次只会执行一个任务。任务完成后会检查是否还有空闲时间,有的话会继续执行下一个任务,没有则等到下次有空闲时间再执行。
需要注意的是此队列中的任务也有可能阻塞页面,当空闲时间用完后任务不会主动退出。如果任务占用时间较长,一般会将任务拆分成多个阶段,执行完一个阶段后检查还有没有空闲时间,有则继续,无则注册一个新的 idle 队列任务,然后退出当前任务。