JavaScript
数据类型
原始数据类型:Number、String、Boolean、Null、Undefined、Symbol
引用数据类型:Object、Function、Array
类型检测
-
typeof:只能检测除 null 以外的其他原始数据类型;引用类型和 null 都会返回 "object"
var a = 123; typeof a; -
instanceof:基于原型链实现,判断某个实例是否是由某个构造函数生成,返回 true 或 false.所有引用数据类型的值都是 Object 的实例。
a instanceof b
- 实现 instanceof
function instanceof(left, right){
const rightPrototype = right.prototype;
const leftProto = left.__proto__;
while(true){
if (leftProto === null) {
return false;
}
if (leftProto === rightPrototype) {
return true;
}
leftProto = leftProto.__proto__;
}
}
-
Array.isArray():判断数组类型
-
Object.prototype.toString.call():最准确,输出类似 [object Number] 的格式
Object.prototype.toString.call(a) -
如何判断 NaN:
- Number.isNaN(value)
- Object.is(NaN, value)
类型转换
== 做比较时:
- Boolean == ?,Boolean 转为 Number
- String == Number,String 转为 Number
- String == Boolean,String 转为 Number,Boolean 转为 Number
- Object == ?,调用 valueOf() 将 Object 转为基本类型之后,再按照上面规则比较,转换失败返回 NaN
- valueOf:返回对象的原始值,没有原始值就返回对象本身
- 数组返回本身
- Date 返回毫秒形式的时间戳
- 函数和对象返回本身
- Boolean、Number、String 返回各自的值
- toString:返回对象的字符串。
- 数组重写了 toString 方法,返回逗号分隔的字符串
- Date 重写了 toString 方法,返回日期的文字表示
- 函数返回 'function demo(){console.log(1);}'
- Boolean、Number、String 返回各自用字符串表示的值
- 数值运算优先 valueOf,字符串运算优先 toString
- undefined 和 null 没有 toString 和 valueOf 方法
- valueOf:返回对象的原始值,没有原始值就返回对象本身
- NaN 和任何类型(包括自己)比较都返回 false
undefined == null => true,除此之外,undefined 和 null 跟其他任何值比较都是 false
-、/、% 做运算时:
- 转化为 Number 运算
+做运算时:
- 加号一侧为字符串,则另一侧转换为字符串做拼接
- 加号一侧为数字,另一侧为原始类型,则原始类型转换为 Number 做运算
- NaN、undefined 转换为数字是 NaN
- false、null 转换为数字 0
- 加号一侧为数字,另一侧为引用类型,先调用 valueof 获取原始值,没有则使用 toString,再进行下一步计算
实现无限累加函数
function add(n){
function _add(m){
n = n + m;
// 每次调用完都返回 _add 方法,以便下次调用
return _add;
}
// 加号运算符还会导致隐式的调用到 valueOf 或 toString 方法
// 这里重写 toString 来达到返回累加后的数据的目的
_add.toString = function(){
return n;
}
return _add;
}
// 使用+运算符触发 toString 方法的调用
console.log(+add(1)(2)(3)); // 6
升级版(不限参数个数)
function add(){
let args = [...arguments];
function _add(){
const _args = [...arguments];
return add.apply(null, [...args,..._args]);
}
_add.toString = () => {
return args.reduce((pre, curr)=> pre + curr);
}
return _add;
}
原型
prototype:
- 只要创建一个函数,就会为这个函数创建一个 prototype 属性
- 只有函数才有 prototype 属性
_proto_:
- 对象都有这个属性,指向它的构造函数的 prototype 原型链:
- 当视图访问对象的某个属性,则会先查找对象本身的属性,若本身没有,则在该对象的原型里去查找,逐级向上,直到原型链的顶端 null
- 原型链的顶端是 null
- 每个对象都有一个
__proto__属性,指向它构造函数的原型对象 prototype,构造函数的原型对象也有它自己的__proto__属性,逐级向上直到一个对象的原型指向 null,这样一层一层的关系就叫做原型链。
原型相关的方法
a instanceof b判断 a 是否是 b 的一个实例a.hasOwnPropertyOf(b)判断 b 是否是 a 自己定义的方法(可以用来判断排除原型上的方法)Object.getPrototypeOf(a)获取 a 的原型上的方法
继承
原型链继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = () => {
console.log("hi");
}
function Student(name, age) {
this.name = name;
this.age = age;
}
Student.prototype = new Person();
优点:
- 可以继承父类原型链上的属性
- 子类型创建不能向父类传递参数 缺点:
- 如果原型链上的属性是引用类型,那么所有的实例都共享一个引用,一个实例改变引用类型的值,其他的实例也会受到影响
借用构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
function Student(name, age) {
Person.call(this, name, age);
}
var student = new Student("xiaoming", 12);
优点:
- 子类型创建能向父类传递参数
- 各实例的属性保持独立,不会互相影响 缺点:
- 不能继承父类原型链上的属性
组合继承
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.say = function(){
console.log("hi")
}
function Student(name, age){
Person.call(this, name, age);
}
Student.prototype = Object.create(Person);
Student.prototype.constructor = Student;
优点:
- 使用原型链实现对原型属性和方法的继承,借用构造函数实现对实例属性的继承,结合了两者的优点
ES6 继承
class Parent {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
class Student extends Parent {
constructor(name, age) {
super(name, age);
}
}
ES5 继承和 ES6 继承的差别
- es5 会先创建子类型的构造函数实例,然后通过 call 继承父类属性,因此无法继承原生的构造函数
- es6 先创建父类的 this,然后通过修饰子类型的 this 实现继承,因此可以继承原生的构造函数
几个关于创建对象的方法
Object.create(A)
- 创建一个新对象,并继承 A 的原型 new Object()
- 同 {}
new 操作符
- 创建一个空对象
- 将空对象的原型指向目标对象的 prototype
- 改变 this 指向为创建的新对象
- 执行构造函数,返回创建的新对象
function MyNew(o){
var obj = {};
obj.__proto__ = o.prototype;
var result = o.call(obj);
return typeof result === "object" ? result : obj;
}
this
this 永远指向一个对象;this 的指向取决于调用的位置
- 对象调用:谁调用就指向谁
- 直接调用函数:函数内部的 this 指向 window
- new 创建新对象:this 指向新创建的对象
- 定时器:this 指向 window
- 箭头函数:定义函数时所在的上下文,而不是调用时的上下文
apply、call
都能改变 this 的指向,第一个参数是 this 新指向的对象,apply 接受一个数组作为第二个参数,call 需要将参数平铺依次传入
a.apply(b, [1, 2, 3]);
a.call(b, 1, 2, 3);
bind
改变 this 的指向,并返回一个新函数
var newFn = a.bind(b, 1, 2, 3);
实现 bind
// 基础版
function bind(){
var newObj = arguments[0];
var args = Array.prototype.slice.call(arguments, 1);
var self = this;
return function(_args){
return self.apply(newObj, [...args, ..._args]);
}
}
// 升级版,支持 new 操作符创建对象
function bindPro() {
var newObj = arguments[0];
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fn = function(){};
var newFn = function(_args){
return self.apply(self instanceof fn ? this : newObj, [...args, _args]);
}
fn.prototype = this.prototype;
newFn.prototype = new fn();
return newFn;
}
执行上下文
定义:当前代码的执行环境 分三类:
- 全局执行上下文:最外围的执行环境,在浏览器里指 window
- 函数执行上下文:可以有无数个,当函数调用的时候被创建,每次调用会产生一个新的上下文
- eval 执行上下文 执行上下文的三个属性:
- 变量对象:执行环境里定义的所有变量和函数都保存在变量对象中。
- 活动对象:活动对象就是变量对象,只不过是当进入一个执行环境时,变量对象会被激活,因此叫活动对象,只有活动对象中的属性是可访问的
- 作用域链:当代码在一个环境中执行时,会创建变量对象的一个作用域链,作用域链保证变量的有序访问
- this
执行上下文生命周期过程:
- 创建变量对象
- 初始化函数参数 arguments
- 函数声明
- 变量声明
- 创建作用域链
- 函数作用域链在定义的时候就确定了。查找变量时,会先从当前上下文的变量对象中查找,没找到则从父级执行上下文中的变量对象中查找
- 确定 this 的指向
- 变量赋值,代码执行
- 执行上下文出栈,回收
作用域
定义:是用于确定在哪里找,怎么找到某个变量的一套规则
词法作用域:在写代码时将变量写在哪里决定的。编译的词法分析阶段能够知道全局标识符在哪里以及如何声明
变量提升:指变量的声明在它的整个作用域范围内都存在
- 包含函数和变量在内的所有声明都会在任何代码执行之前被处理
- 函数声明会被提升,但是函数表达式不会,也就是说使用
function fun(){}声明的函数,可以在声明它之前就使用,但是var fun = function(){}的不可以,因为他的值是undefined
块级作用域:指在块级作用域以外,无法访问块级作用域内部的变量。
- 在 es6 之前,除了函数体,没有块级作用域
- es6 中,{} 可以创建块级作用域,
let、const可以创建块级作用域变量
作用域链:变量对象形成的链表。当查找某个变量时,会从第一个变量对象中查找,若没有这个属性,就查找链上的第二个变量对象,若最终没有找到则会抛出一个错误。
闭包
闭包:一个函数和其周围的词法环境的引用捆绑在一起,闭包能让内部函数访问外层函数的作用域。每当创建一个函数,闭包就会同时被创建出来 场景:
- 将函数作为值返回
- IIFE 会保存全局作用域和当前函数作用域
- 定时器、时间监听 作用:
- 借助闭包封装私有变量
- 把多参数函数变成单参数的函数(实现柯里化) 缺点:
- 因为闭包对外部作用域存在引用,因此外部作用域不会被销毁,可能会造成内存泄露
Event Loop
javascript 是一门单线程的语言,即 js 在执行的任何的时候,只有一个主线程来处理所有的任务。那整个运行过程中的任务,就需要有一个调度机制来保证有序执行,这个机制就叫做事件循环。
执行栈
从执行上下文的知识里我们知道,当我们调用一个方法或函数的时候,会生成一个对应的执行上下文,当一系列方法被调用的时候,就会生成很多个执行上下文,因为 js 是单线程,所以这些执行上下文会被排在一个栈里,依次出栈调用,这个栈叫做执行栈。
事件队列
当 js 遇到一个异步事件后,不会一直等待它返回结果,而是会将事件挂起继续执行执行栈中的任务,等异步事件结果返回之后,会将事件加入另一个队列,这个队列叫事件队列。在一个事件循环中,会根据事件的类型,将事件分成宏任务和微任务,分别放到宏任务队列和微任务队列中
-
宏任务
- setInterval
- setTimeout
-
微任务
- Promise
- MutationObserver
当前执行栈执行完毕后,会优先处理完微任务队列中所有事件,然后从宏任务队列中取出最前面的任务执行,同一次事件循环中,微任务永远优先于宏任务执行
ES6 相关
块级作用域声明 let、const
- var
- var 关键字允许重复声明
- 使用 var 声明的变量会被提升,也就是可以在声明一个变量之前使用该变量,初始化为 undefined,创建和初始化一起被提升;
- 1、创建的同时进行初始化 undefined 2、赋值
- let、const
- 不允许重复声明
- 使用 let 声明的变量也会被提升,但是不可以提前使用,会报错
- let 声明的变量在环境实例化的时候被创建,但是在变量的词法绑定前不允许使用。创建被提升了,但是初始化没被提升。从 let 创建到被初始化这部分,变量时不可用的,也就是 「暂时性死区」
- 暂时性死区的本质:只要一进入当前作用域,变量就已经存在了,但是不可获取,只有等到声明的那一行代码出现,才可以使用;
- 存在于独立的块级作用域中,内部的变量声明会覆盖外部的变量声明,因此下面这段代码会报错
let a = 123;
if (true) {
console.log(a); // Uncaught ReferenceError: Cannot access 'a' before initialization
let a = 456;
}
数组方法
- find():返回第一个匹配的元素的值
- includes():若存在给定的值,返回 true
- flat(depth):扁平化数组,默认只扁平一级,Infinity 表示完全打平所有嵌套层级
- fill(value, start, end):用给定的值填充数组,
- keys():返回数组键组成的迭代器(注意不是数组)
- values():返回数组值组成的迭代器(注意不是数组)
- Array.from(p1, mapFn):p1是任意可迭代的对象或类数组,使用它们的值来构建数组;mapFn 是一个映射函数,处理每个数组元素,返回新的值
for-in 和 for-of
- for-in:循环可枚举对象
- for-of:遍历迭代器对象(map、set、string、伪数组、generator)
Set & Map
- Set:唯一值的集合。
- 参数:一个可迭代的对象。
- 创建:
const s = new Set(["one","two"]); - 添加:
s.add("tree").add("four"); - 判断是否包含:
s.has("one") === true - 删除:
s.delete("one") - 长度:
s.size() - 转换成数组:
Array.from(s);
- Map:一个事物到另一个事物的映射
- 参数:可迭代的对象
- 创建:
const m = new Map([["a", "one"],["b", "two"]]); - 添加:
m.set("c", "tree"); - 获取:
m.get("a"); - 删除:
n.delete("c"); - 遍历:
for(const [key,value] of m){} - 与对象的区别
- 对象的键只能是字符串,Map 可以是任意值
- 对象做映射取值的时候不能保证顺序
- WeakMap
- 只接受对象作为键
- WeakMap 的键是弱引用,因此键所指向的对象是可以被回收的
- 不能被遍历
垃圾回收
引用计数
给一个占用物理空间的对象加一个计数器,被引用一次就 +1,反之 -1,当计数为 0 的时候就会被回收
标记清除
遍历调用栈,被引用的对象标记为活动对象,没有被引用的对象标记为垃圾对象,垃圾对象会被清理掉
- 在开发过程中,想要回收某个对象,只需要将它置为 null。但是当对象作为另一个对象的键或值,就不会被回收。
Vue 相关
Vue2和Vue3响应式的区别
-
vue2 详解
- 在初始化的时候会传入一个data数据对象,vue会遍历这个对象,将对象的所有属性都通过
Object.defineProperty()转化为getter/setter;
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() {}, set: function reactiveSetter(newVal) {} })- 使用
Observer将普通数据转化为可观测的数据- 给value打上响应式属性的标记
__ob__ - 判断数据类型,对象就使用
defineReactive()创建响应式对象,数组就遍历数组,使用Observer()对每一个元素进行监听
- 给value打上响应式属性的标记
- 上述的属性转化只有在组件实例初始化的时候发生,因为如果是创建之后的实例直接添加属性,那属性就没有响应性,要使用set方法添加新的属性;
let vm = new Vue({ data: { hello: 'hello' } }) vm.hello = 'world'; // 响应式 vm.world = 'world'; // 添加根级别的属性,将不是响应式 - 在初始化的时候会传入一个data数据对象,vue会遍历这个对象,将对象的所有属性都通过
-
vue3
- 使用proxy包装创建响应式对象
// target 为组件的 data 返回的对象 new Proxy(target,{ get(target, key){}, set(target, key, value){} })- 需要显式的声明响应式对象,使用
ref()或reactive()
Observer、Dep、Watcher的作用 详解1 详解2
- Observer 的作用
- 观察者,观察的是data,通过数据劫持将data的读写都处于监管之下
- 递归data,将data对象和子对象添加
__ob__属性并通过defineReactive()为属性定义getter/setter
- Dep 的作用
- 依赖的管理者,subs就是依赖(订阅者)列表,每一个都是
Watcher的实例
- 依赖的管理者,subs就是依赖(订阅者)列表,每一个都是
// src/core/observer/dep.js
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {}
removeSub (sub: Watcher) {}
depend () {}
notify () {}
}
- Watcher 的作用
- 订阅者,实例化Watcher的时候会触发getter,从而执行dep.depend()将当前Watcher加入Dep维护的依赖列表,这就是依赖收集
Promise
使用场景:Promise 是为了解决 js 回调嵌套过多的问题而产生的。它支持链式调用,使用更方便。 规范:Promise 遵循 Promise/A+ 规范
- pending:表示初始状态,可以转移到 rejected 或 fulfilled 状态
- fulfilled:表示操作成功,不可转移状态
- rejected:表示操作失败,不可转移状态
- 必须有一个 then 异步执行方法,且接受两个参数并且返回 promise
实现思路
- 几个变量
- status:保存 promise 实例的状态
- reason:保存 rejected 之后的原因
- value:保存 fulfilled 之后的值
- fulfilledCallbacks:fulfilled 回调队列
- rejectedCallbacks:rejected 回调队列
- resolve 和 reject 方法作为 executor 函数的两个参数
- 在 then 方法中返回新的 promise 实例;判断 status 是否是 pending,如果是,则将成功和失败的回调分别加入队列
- 在 resolve 和 reject,方法中,判断 status 是否为 pending,如果是,则分别改为 fulfilled 和 rejected,并批量执行回调队列中的方法
const PENDING = "pending";
const FULFILLED = "fulfilled";
const REJECTED = "rejected";
function Promise1(executor){
this.status = PENDING;
this.value = "";
this.reason = "";
this.fulfiledCallbacks = [];
this.rejectCallbacks = [];
function resolve(value){
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
this.fulfilledCallbacks.forEach(function(cb){
cb(this.value);
});
}
}
function reject(reason){
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
this.rejectCallbacks.forEach(function(cb){
cb(this.reason);
});
}
}
try{
excutor(resolve, reject);
}catch(e){
reject(e);
}
}
Promise1.prototype.then = function(onFulfilled, onReject){
return new Promise1(function(resolve, reject) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
}
if (this.status === REJECTED) {
onReject(this.reason);
}
if (this.status === PENDING) {
this.fulfiledCallbacks.push(function(){
var result = onFulrilled(this.value);
resolve(result);
});
this.rejectCallbacks.push(function(){
var result = onReject(this.reason);
reject(result);
});
}
});
}
// 异常捕获
Promise1.prototype.catch = function(onReject){
this.then(null, onReject);
};
Promise1.resolve = function(value){
return new Promise1(function(resolve){
resolve(value)
});
}
Promise1.all = function(promises){
return new Promise1((resolve, reject) => {
let pLength = promises.length;
let resolveResult = [];
let resolveNum = 0;
for (let i = 0; i < pLength; i ++) {
promises[i].then((res) => {
resolveResult.push(res);
resolveNum ++;
if (resolveNum === pLength) {
resolve(resolveResult);
};
}).catch((e) => {
reject(e);
});
}
});
}
Promise1.race = function(promises){
return new Promise1((resolve, reject) => {
promises.forEach((p) => {
p.then(resolve, reject);
});
});
}
var p = new Promise1(function(resolve, reject){
resolve(1);
})
.then(function(value){
console.log(value);
});
async、await 的理解
async/await 是一种更方便完成异步调用的语法。
- async/await 是 generator 的语法糖,async 使得函数始终返回一个 promise,await 必须在 async 内部使用
- 与 generator 相比,它内置了执行器,不用手动调 next() 方法;await 后面可以是普通方法也可以时候 promise,而 yield 后面必须是 promise 或 thunk 函数
原理
async 函数其实是将 generator 函数和自动执行器包装在一个函数里。
generator
generator 封装了多个内部状态。执行 generator 会返回一个遍历器对象,可以调用 next() 依次访问内部的每个状态,因此其实是提供了一个可以暂停执行的函数,yield 就是暂停执行的标识。只不过需要手动调用 next() 来执行下一步,因此需要封装一个自动执行器。
// 基于 promise 的简易自动执行器
function run(gen){
const g = gen();
function next(value){
const result = g.next(value);
if (result.done) {
return result.value;
}
// 只要没执行完,就继续调用自身
result.value.then((res) => {
next(res);
});
}
next();
}
function* foo() {
let response1 = yield fetch('https://xxx') //返回promise对象
console.log(response1)
let response2 = yield fetch('https://xxx') //返回promise对象
console.log(response2)
}
run(foo);
CSS
BFC
块级格式化上下文。是一块独立的渲染区域,用于决定块元素布局及浮动相互影响范围的一块区域 触发规则
- 根元素:HTML 标签
- 浮动元素:float 为 left、right
- overflow 的值不为 visible
- display 的值为 table-cell、inline-block、flex、inline-flex
- position 值为 absolute、fixed BFC 区域的布局规则:
- BFC 区域内部的元素会一个接一个垂直排列
- 属于同一个 BFC 的两个垂直相邻的盒子,上下 margin 会发生重叠,取较大的一个 margin
- BFC 的区域不会与 float 区域发生重叠(可用作浮动的清除)
- 计算 BFC 区域高度时,float 元素的高度也参与计算
浮动
浮动会是元素脱离文档流,按照设定的方向(left、right)移动,直到接触到包含框的边缘或另一个浮动元素的边缘
- 浮动可以设置三个属性,float:left、right、none; float 元素的特性
- 行内元素围绕浮动元素摆放:浮动最初设计的目的就是为了文字环绕图像
- 父元素高度塌陷:浮动元素的高度不会被计算
- 块元素认为浮动元素不存在:因此跟在浮动元素后面的块元素会被前面的浮动元素遮挡;但是浮动元素前面的块元素不会被后面的浮动元素遮挡;
- 浮动元素一个挨着一个摆放 清除浮动
- BFC
- 给浮动元素后面添加伪元素
.clear::after {
content: "";
clear: both;
display: block;
}
定位
position:relative
- 相对定位,相对于自己最初的位置定位。
- 相对定位的元素偏移之后依然占据原来的位置,也不会挤开别的元素 position:absolute
- 绝对定位,相对于最近的一个设置了 position 不为 static 的父元素定位,如果没有,则相对于 body 定位
- 绝对定位的元素不占据原来的位置 position:fixed
- 固定定位,以浏览器窗口为参考进行定位
- 脱离文档流,不跟随页面滚动二发生变化 position:sticky
- 粘性定位,可以理解为 relative + fixed 的结合效果,必须结合 tlrt 属性使用
- 当页面开始滚动,父元素开始脱离视口时,只要与 sticky 元素的距离达到生效距离,sticky 元素就会表现为 fixed 定位
transform 和直接使用 top、left 改变位置有什么优缺点
- top、left 是布局类样式,改变会导致重排
- transform: translate(x,y) 只是导致重绘
- 总结就是 transform 性能更好,top、left 兼容性更好