前端知识点-基础(持续更新...)

220 阅读18分钟

JavaScript

数据类型

原始数据类型:Number、String、Boolean、Null、Undefined、Symbol

引用数据类型:Object、Function、Array

类型检测

  1. typeof:只能检测除 null 以外的其他原始数据类型;引用类型和 null 都会返回 "object" var a = 123; typeof a;

  2. 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__;
    }
}
  1. Array.isArray():判断数组类型

  2. Object.prototype.toString.call():最准确,输出类似 [object Number] 的格式 Object.prototype.toString.call(a)

  3. 如何判断 NaN:

  • Number.isNaN(value)
  • Object.is(NaN, value)

类型转换

== 做比较时:

  1. Boolean == ?,Boolean 转为 Number
  2. String == Number,String 转为 Number
  3. String == Boolean,String 转为 Number,Boolean 转为 Number
  4. 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 方法
  5. NaN 和任何类型(包括自己)比较都返回 false
  6. undefined == null => true,除此之外,undefined 和 null 跟其他任何值比较都是 false

-、/、% 做运算时:

  1. 转化为 Number 运算

+做运算时:

  1. 加号一侧为字符串,则另一侧转换为字符串做拼接
  2. 加号一侧为数字,另一侧为原始类型,则原始类型转换为 Number 做运算
    • NaN、undefined 转换为数字是 NaN
    • false、null 转换为数字 0
  3. 加号一侧为数字,另一侧为引用类型,先调用 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,这样一层一层的关系就叫做原型链。

prototype.webp

原型相关的方法

  • 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 操作符

  1. 创建一个空对象
  2. 将空对象的原型指向目标对象的 prototype
  3. 改变 this 指向为创建的新对象
  4. 执行构造函数,返回创建的新对象
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

执行上下文生命周期过程:

  1. 创建变量对象
    • 初始化函数参数 arguments
    • 函数声明
    • 变量声明
  2. 创建作用域链
    • 函数作用域链在定义的时候就确定了。查找变量时,会先从当前上下文的变量对象中查找,没找到则从父级执行上下文中的变量对象中查找
  3. 确定 this 的指向
  4. 变量赋值,代码执行
  5. 执行上下文出栈,回收

作用域

定义:是用于确定在哪里找,怎么找到某个变量的一套规则

词法作用域:在写代码时将变量写在哪里决定的。编译的词法分析阶段能够知道全局标识符在哪里以及如何声明

变量提升:指变量的声明在它的整个作用域范围内都存在

  • 包含函数和变量在内的所有声明都会在任何代码执行之前被处理
  • 函数声明会被提升,但是函数表达式不会,也就是说使用 function fun(){}声明的函数,可以在声明它之前就使用,但是 var fun = function(){} 的不可以,因为他的值是 undefined

块级作用域:指在块级作用域以外,无法访问块级作用域内部的变量。

  • 在 es6 之前,除了函数体,没有块级作用域
  • es6 中,{} 可以创建块级作用域,letconst可以创建块级作用域变量

作用域链:变量对象形成的链表。当查找某个变量时,会从第一个变量对象中查找,若没有这个属性,就查找链上的第二个变量对象,若最终没有找到则会抛出一个错误。

zuoyongyulian.png

闭包

闭包:一个函数和其周围的词法环境的引用捆绑在一起,闭包能让内部函数访问外层函数的作用域。每当创建一个函数,闭包就会同时被创建出来 场景:

  • 将函数作为值返回
  • 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()对每一个元素进行监听
    • 上述的属性转化只有在组件实例初始化的时候发生,因为如果是创建之后的实例直接添加属性,那属性就没有响应性,要使用set方法添加新的属性;
    let vm = new Vue({ data: { hello: 'hello' } }) 
    vm.hello = 'world'; // 响应式 
    vm.world = 'world'; // 添加根级别的属性,将不是响应式
    
  • 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 的实例
// 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 兼容性更好