js基础学习整理

153 阅读11分钟

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.stateHistory 堆栈最上层的状态值
// 后退到前一个网址
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 = 1export { m };
// 写法3
const m = 1export { m as module };


// module.js
// 其实export default就是export { xxx as default }
const m = 1export 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]'

以下视为加号运算符

  • 加号两边都为数字类型

  • 加号两边都是基本类型,除了字符串类型,则可视为加号。也就是说加号两边可以为BooleanNumbernullundefined这种情况,肯定是加号运算符。将对不是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携带问题:
  1. fetch方法不管跨不跨域,默认都不携带,需要设置credentials,且服务端需要设置响应头 Access-Control-Allow-Credentials: true, 否则浏览器会因为安全限制而报错, 拿不到响应
fetch(url, {
    credentials: "include", // include(都带), same-origin(同源下携带), omit(默认,不携带)
})
  1. axios和jQuery在同域ajax请求时会带上cookie, 跨域请求不会, 跨域请求需要设置 withCredentials 和服务端响应头
axios.get('http://server.com', {withCredentials: true})

7. 一帧的执行

参考 segmentfault.com/a/119000001…

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 队列任务,然后退出当前任务。