JavaScript 核心原理精讲-学习记录

87 阅读14分钟

1、js的数据类型

1.1 概念

  1. js的数据类型共有八种如下图 image.png
  2. 前7种类型为基础类型,最后一种Object为引用类型,而引用类型又分为这几种常见的类型:数组对象、正则对象、日期对象、数学函数、函数对象
  3. js数据类型最后会在解释器解析过程中存放在不同的内存中,因此上面的数据可以分成两类进行存储:
    1. 基础类型存储在栈内存中,随着出栈而被销毁同时释放内存
    2. 引用类型存储在堆内存中,同时地址存储在栈内存中被引用,当出栈时,引用销毁后,堆内存的数据在垃圾回收时也会被销毁
  4. String、Number、Boolen又被称为基本包装类型,js在读取该类型时会自动创建一个基本包装类型对象,这样就可以执行类似toString的函数

1.2 数据类型检测

  1. typeof可以用来检测基础类型(除了null)和引用类型中的function,其余的引用类型都会返回'object'无法具体判断。null也会返回object,某些文章说是语言设计遗留的bug。(编程语言最后的形式都是二进制,所以 JavaScript 中的对象在底层肯定也是以二进制表示的。如果底层有前三位都是零的二进制,就会被判定为对象。底层中 null 的二进制表示都是零。所以在对 null 的类型判定时,发现其二进制前三位都是零,因此判定为 object)
  2. instanceof用来检测对象是否与指定对象有相同原型,可用于检测引用类型
  3. Object.prototype.toString.call()统一返回'[object Xxx]'的字符串, 其中Xxx就是对象的类型.

1.3 数据类型转换

  1. 当一个操作值为object且另外一方为string, number,symbol,就会把object转换为原始类型再进行判断(即调用object的valueOf/toString方法进行转换) 如下经典🌰:
var a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
// 注意这里a又可以等于1、2、3
console.log(a == 1 && a == 2 && a ==3);  //true 规则 Object隐式转换
  1. Object的转换规则及优先级
    1. 如果部署了Symbol.toPrimitive方法,优先调用再返回;
    2. 调用 valueOf(), 如果转换为基础类型,则返回;
    3. 调用toString(), 如果转换为基础类型,则返回;
    4. 如果都没有返回基础类型,会报错

2、如何实现-个深浅拷贝

2.1 浅拷贝

  1. 浅拷贝可以理解为创建一个新的对象来接受要复制或者引用的对象值。如果对象属性是基本数据类型就复制值,但如果属性是引用数据类型就复制引用的内存地址,此时如果内存中的值被修改,都会受到影响
  2. 原生方法:object.create、object.assign、扩展运算符({...obj},[...arr])、concat拷贝数组、slice拷贝数组
  3. 注意🤔
    1. Object.create采用的是通过将目标对象赋值给创建对象的原型完成的对象复制,这样导致新对象的属性都不可枚举。
    2. Object.assign不会拷贝对象的继承属性,不会拷贝不可枚举属性。因为内部做了hasOwnProperty判断.也可以拷贝数组
    3. concat和slice都不会改变原数组
  4. 手写一个Object.assign
Object.defineProperty(Object, 'assign1', {
    value(target, ...sources){
        if (target == undefined) {
            throw new TypeError('Cannot convert undefined or null to object')
        }
        if (typeof target != 'object') return target;
        for(let i = 0; i < sources.length; i++) {
            const source = sources[i]
            for(let k in source) {
                if (source.hasOwnProperty(k)) {
                    target[k] = source[k]
                }
            }
        }
        return target
    }
})

2.2 深拷贝

  1. 深拷贝之于浅拷贝的区别在对引用类型的处理,深拷贝是在内存中重新生成空间存放依此解决对象之间的关联,二者实现真正的分离。
  2. 原生方法:JSON.stringify/JSON.parse
  3. 注意🤔
    1. 对象中如果有函数、undefined、symbol这几种类型,经过JSON.stringify序列化之后键值会消失
    2. 无法拷贝不可枚举、原型链、对象的循环引用(obj[key]=obj)
    3. Date会变成字符串;RegExp变成空对象;NaN、Infinity会变成null
  4. 手写深拷贝
function deepClone(obj, hash = new WeakMap()) {
    if (obj.constructor === Date) return new Date(obj)
    if (obj.constructor === RegExp) return new RegExp(obj)
    if (hash.has(obj)) return hash.get(obj)
    // 创建一个和目标对象有相同原型和相同自有属性描述的cloneData
    // 相同的自由属性描述可以实现不可枚举的属性拷贝后同样不可枚举
    const cloneData = Array.isArray(obj) ? [] : Object.create(Object.getPrototypeOf(obj), Object.getOwnPropertyDescriptors(obj)
    hash.set(obj, cloneData)
    for (const k of Reflect.ownKeys(obj)) {
        cloneData[k] = typeof obj[k] !== 'object' || obj[k] === null  ? obj[k] : deepClone(obj[k], hash)
    }
    return cloneData
}
  1. 注意🤔
    1. Refect.ownKeys方法解决不可枚举的属性
    2. 利用Object.create,Object.getPrototypeOf,Object.getOwnPropertyDescriptors实现对原型链的拷贝
    3. 利用WeakMap类型作为Hash表,因为WeakMap是弱引用类型,可以有效防止内存泄露。(需要深入了解WeakMap为什么可以防止内存泄露)。通过Hash表解决循环引用问题。

2.3 概念扩展

  1. WeakMap是弱引用类型

WeakMaps hold "weak" references to key objects

翻译过来应该是 WeakMaps 保持了对键名所引用的对象的弱引用。

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。 一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

通俗来讲就是垃圾回收执行时,被强引用的内存不会回收,被弱引用的内存会回收。 参考链接:ES6 系列之 WeakMap

3、探究 JS 常见的 6 种继承方式

3.1. 原型链继承

原型链继承是比较常见的继承方式之一,其中涉及的构造函数、原型和实例,三者之间存在着一定的关系,即每一个构造函数都有一个原型对象,原型对象又包含一个指向构造函数的指针,而实例则包含一个原型对象的指针。

// 原型链继承
function Parent1() {
    this.name = 'parent1';
    this.play = [1, 2, 3]
}
function Child1() {
    this.type = 'child1';
}
function Clild2() {
    this.type = 'child2'
}
Child1.prototype = new Parent1();
Child2.prototype = new Parent1();
var s1 = new Child1();
var s2 = new Child2();
s1.play.push(4);
console.log(s1.play, s2.play); // s1和s2返回相同的结果

3.2. 构造函数继承

只继承了父类的属性,不继承父类的原型

// 构造函数继承
function Parent1(){
    this.name = 'parent1';
}

Parent1.prototype.getName = function () {
    return this.name;
}

function Child1(){
    Parent1.call(this);
    this.type = 'child1'
}

let child = new Child1();
console.log(child);  // 没问题
console.log(child.getName());  // 会报错

3.3 组合继承

根据上面的两种继承方式的优缺点结合起来,生成新的继承方式

// 组合继承
function Parent3 () {
    this.name = 'parent3';
    this.play = [1, 2, 3];
}

Parent3.prototype.getName = function () {
    return this.name;
}
function Child3() {
    // 第二次调用 Parent3()
    Parent3.call(this);
    this.type = 'child3';
}
// 第一次调用 Parent3()
Child.prototype = new Parent3()
// 手动挂上构造器,指向自己的构造函数,不然子类new的实例的构造函数指向Parent3
Child3.prototype.constructor = Child3;
var s3 = new Child3();
var s4 = new Child3();
s3.play.push(4);
console.log(s3.play, s4.play);  // 不互相影响
console.log(s3.getName()); // 正常输出'parent3'
console.log(s4.getName()); // 正常输出'parent3'
// 至此,属性和原型都继承了,同时不会互相影响,看起来完美。但是细心观察会发现,Parent3函数被调用了两次,那么有没有办法只调用一次呢? 那我们带着问题往下看

3.4 寄生组合式继承

由于call已经继承了属性,那么我们就没必要通过将子类的原型指向父类实例的方式去继承原型及属性,只需要继承父类的原型就可以了

function clone (parent, child) {
    // child.prototype = parent.prototype
    // 请思考🤔为什么要使用create函数将父类原型生成新的原型对象再赋值给子类,而不是子类原型直接指向父类原型(关键字,隔离)
    // 这里改用 Object.create 就可以减少组合继承中多进行一次构造的过程
    child.prototype = Object.create(parent.prototype);
    child.prototype.constructor = child;
}

function Parent4() {
    this.name = 'parent4';
    this.play = [1, 2, 3];
}
 Parent4.prototype.getName = function () {
    return this.name;
}
function Child4() {
    Parent6.call(this);
    this.friends = 'child4';
}

clone(Parent4, Child4);

Child4.prototype.getFriends = function () {
    return this.friends;
}

let person4 = new Child4();
console.log(person4);
console.log(person4.getName());
console.log(person4.getFriends());

3.5. ES6的语法糖extents

我们使用babel编辑工具编译extents的代码发现,其内部使用的继承方式就是上面的第四种寄生组合式继承,由此证明寄生组合式继承是继承实现的较优选择

4、如何实现 new、apply、call、bind 的底层逻辑

4.1 new原理介绍

new 关键词执行之后总是会返回一个对象,要么是实例对象,要么是 return 语句指定的对象。

  1. 创建一个新对象
  2. 将构造函数的作用域给新对象(this指向新对象)
  3. 执行构造函数中的代码(为这个新对象添加属性)
  4. 返回新对象
  5. typeof child === 'object'
function _new(ctor, ...args) {
    if (typeof ctor !== 'function') {
        throw new TyperError('ctor must be a funciton')
    }
    let obj = {};
    obj.__proto__ = Object.create(ctor.prototype);
    let res = ctor.apply(obj, [...args]);
    let isObject = typeof res === 'function' || (typeof res === 'object' && res !== null)
    return isObject ? res : obj
}

4.2 apply & call & bind原理介绍

all、apply 和 bind 是挂在 Function 对象上的三个方法,调用这三个方法的必须是一个函数。这三个方法的作用都是改变函数的this指向。

// 由于 apply 和 call 基本原理是差不多的,只是参数存在区别,因此我们将这两个的实现方法放在一起讲。
Function.prototype.call = function (context, ...args){
    // 如果不让用...args结构,可以将arguments类数组转换成数组
    const context = context || window;
    context.fn = this;
    const res = eval('context.fn(...args)');
    delete context.fn
    return res;
};
Function.prototype.apply = function (context, args) {
    const context = context || window;
    context.fn = this;
    const res = eval('context.fn(...args)');
    delete context.fn;
    return res;
};

// bind的实现思路基本和apply一样,但是最后返回的不是函数的执行结果,而是返回一个新的函数
Function.prototype.bind = function(context, ...args) {
    if (typeof this !== 'function') {
        throw new TypeError('this must be a function');
    }
    const self = this
    const bound = function() {
        self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
    }
    if (this.prototype) {
        bound.prototype = Object.create(this.prototype);
    }
    return bound
}

image.png

5、数组

5.1 数组API

  1. 构造数组API
    1. array(...args) 当args长度大于1时,参数作为数组的元素,当args长度为1且为数字时,会生成长度为该数值的数组,当args长度为1且不为数字时,作为数组的元素
    2. []
    3. Array.of(...args) 将参数依次转化为数组中的一项,然后返回这个新数组
    4. Array.from(target, function, thisObj) 将类数组对象转化为数组。function可自定义迭代器函数对数据做处理(非必填),thisObj用来绑定function的this指向
  2. 可改变数组自身的API
    1. unshif(...args) 顶部插入元素
    2. shif() 移除第一个元素
    3. push(...args) 底部追加元素
    4. pop() 移除最后一个元素
    5. revert() 将数组中的元素前后顺序反转
    6. splice(start,len, ...args) 可用来删除,替换,插入元素
    7. sort() 排序
    8. fill(value, start非必填默认0, end非必填)用于将一个固定值替换数组的元素。
    9. copyWith(target, start非必填默认0, end非必填可为负值) 从数组的指定位置拷贝元素到数组的另一个指定位置中。
  3. 不改变数组自身的API
    1. concat() 连接两个或更多的数组,并返回新数组
    2. join() 通过指定的连接符生成字符串并返回
    3. slice(start, end) 选取数组的一部分,并返回一个新数组
    4. toString() 返回字符串,默认以逗号连接
    5. indexOf(val) 从头部开始查询指定元素在数组中的位置并返回,没有返回-1
    6. lastIndexOf(val) 同上,区别是从尾部开始查询
    7. includes(val) 判断一个数组是否包含一个指定的值。
  4. 数组遍历API
    1. forEach(function(currentValue, index,arr), thisObj) 数组每个元素都执行一次回调函数。
    2. every(function(currentValue, index,arr), thisObj) 方法用于检测数组所有元素是否都符合指定条件(通过函数提供)。
    3. some(function(currentValue, index,arr),thisObj) 检测数组元素中是否有元素符合指定条件。
    4. filter(function(currentValue, index,arr),thisObj) 返回通过检查指定数组中符合条件新数组
    5. map(function(currentValue, index,arr),thisObj) 返回新数组
    6. reduce(function(total,currentValue, index,arr),initialValue)将数组元素计算为一个值(从左到右)
    7. reduceRight(function(total,currentValue, index,arr),initialValue)将数组元素计算为一个值(从右到左)
    8. entries()返回数组的可迭代对象
    9. find(function(currentValue, index,arr), thisObj)返回通过测试(函数内判断)的数组的第一个元素的值
    10. findIndex(function(currentValue, index,arr), thisObj)返回传入一个测试条件(函数)符合条件的数组第一个元素位置。
    11. keys()从数组创建一个包含数组键的可迭代对象。
    12. values()从数组创建一个包含数组值的可迭代对象。

image.png

5.2 实现数组扁平化的 6 种方式

  1. 普通的递归

通过循环递归的方式,一项一项地去遍历,如果每一项还是一个数组,那么就继续往下遍历,利用递归程序的方法,来实现数组的每一项的连接。

function flatten(arr){
    let arr = []
    arr.forEach(function(itme){
        arr = arr.concat(Array.isArray(item) ? flatten(item) : item)
    })
    return arr
}
  1. 利用 reduce 函数

利用reduce 来实现数组的拼接,从而简化第一种方法的代码

function flatten(arr){
    arr.reduce(function(prev, next){
        prev.concat(Array.isArray(next) ? flatten(next) : next)
    }, [])
}
  1. 扩展运算符实现

用了扩展运算符和 some 的方法,两者共同使用,达到数组扁平化的目的

function flatten(arr){
    while(arr.some(item => Array.isArray(item))) {
        arr = [].concat(...arr)
    }
    return arr
}
  1. split 和 toString 共同处理

由于数组会默认带一个 toString 的方法,所以可以把数组直接转换成逗号分隔的字符串,然后再用 split 方法把字符串重新转换为数组

function flatten(arr) {
    return arr.toString().split(',')
}
  1. ES6 中的 flat

arr.flat([depth]) 其中 depth 是 flat 的参数,depth 是可以传递数组的展开深度(默认不填、数值是 1),即展开一层数组。那么如果多层的该怎么处理呢?参数也可以传进 Infinity,代表不论多少层都要展开

function flatten(arr) {
    return arr.flat(Infinity)
}
  1. 正则和 JSON 方法共同处理

用JSON.stringify函数将数组转化为字符串,然后通过正则表达式过滤字符串中多余的括号

function flatten(arr) {
    let str = JSON.stringify(arr)
    str = str.replace(/(\[|\])/g, '')
    return JSON.parse('[' + str + ']')
}

5.3 手写 JS 数组多个方法的底层实现

你可以先去 ECMA 的官网去查一下关于数组相关方法的基本描述: ECMA 数组方法标准

  1. push方法的实现
When the push method is called with zero or more arguments, the following steps are taken:
1. Let O be ? ToObject(this value).
2. Let len be ? LengthOfArrayLike(O).
3. Let argCount be the number of elements in items.
4. If len + argCount > 2^53 - 1, throwTypeError exception.
5. For each element E of items, do
  a. PerformSet(O, ! ToString(F(len)), E, true).
  b. Set len to len + 1.
6. PerformSet(O, "length"F(len), true).
7. Return F(len).
Array.prototype.push = function(...args) {
    const o = Object(this)
    const len = this.length >>> 0
    const argLen = args.length >>> 0
    // 2^53 - 1 代表的是js的最大安全数,但是从浏览器里实验,数组的最大长度不能超过2^32 - 1,所以这里的逻辑与数组最大长度的实际限制有出入
    if (len + argLen > 2 ** 53 - 1) {
        throw new TypeError("The number of array is over the max value")
    }
    for(let i = 0; i < argLen; i++) {
        o[len + 1] = args[i]
    }
    const newLen = len + argLen
    o.length = newLen
    return newLen
}

  1. pop方法的实现
1. Let `O` be ? [ToObject](https://tc39.es/ecma262/#sec-toobject)(this value).
2. Let `len` be ? [LengthOfArrayLike](https://tc39.es/ecma262/#sec-lengthofarraylike)(`O`).
3. If `len` = 0, then
    a. Perform ? [Set](https://tc39.es/ecma262/#sec-set-o-p-v-throw)(`O`, "length", +0𝔽, true).
    b. Return undefined.
4. Else,
    a. [Assert](https://tc39.es/ecma262/#assert): `len` > 0.
    b. Let `newLen` be [𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(`len` - 1).
    c. Let `index` be ! [ToString](https://tc39.es/ecma262/#sec-tostring)(`newLen`).
    d. Let `element` be ? [Get](https://tc39.es/ecma262/#sec-get-o-p)(`O`, `index`).
    e. Perform ? [DeletePropertyOrThrow](https://tc39.es/ecma262/#sec-deletepropertyorthrow)(`O`, `index`).
    f. Perform ? [Set](https://tc39.es/ecma262/#sec-set-o-p-v-throw)(`O`, "length", `newLen`, true).
    g. Return `element`.
Array.prototype.pop = function() {
    const o = Object(this)
    const len = this.length >>> 0
    if (len === 0) {
        return undefined
    } else {
        const newLen = len - 1
        const element = o[newLen]
        delete o[newLen]
        o.length = newLen
        return element
    }
}

3、map方法的实现

1.Let `O` be ? [ToObject](https://tc39.es/ecma262/#sec-toobject)(this value).
2.Let `len` be ? [LengthOfArrayLike](https://tc39.es/ecma262/#sec-lengthofarraylike)(`O`).
3. If [IsCallable](https://tc39.es/ecma262/#sec-iscallable)(`callbackfn`) is false, throw a TypeError exception.
4. Let `A` be ? [ArraySpeciesCreate](https://tc39.es/ecma262/#sec-arrayspeciescreate)(`O`, `len`).
5. Let `k` be 0.
6. Repeat, while `k` < `len`,
    a. Let `Pk` be ! [ToString](https://tc39.es/ecma262/#sec-tostring)([𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(`k`)).
    b. Let `kPresent` be ? [HasProperty](https://tc39.es/ecma262/#sec-hasproperty)(`O`, `Pk`).
    c. If `kPresent` is true, then
        i. Let `kValue` be ? [Get](https://tc39.es/ecma262/#sec-get-o-p)(`O`, `Pk`).
        ii. Let `mappedValue` be ? [Call](https://tc39.es/ecma262/#sec-call)(`callbackfn`, `thisArg`, « `kValue`, [𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(`k`), `O` »).
        iii. Perform ? [CreateDataPropertyOrThrow](https://tc39.es/ecma262/#sec-createdatapropertyorthrow)(`A`, `Pk`, `mappedValue`).
    d. Set `k` to `k` + 1.
7. Return `A`.
Array.prototype.map = function(callback, thisObject) {
    const o = Object(this)
    const len = this.length >>> 0
    if (typeof callback !== 'function') {
        throw new TypeError(callback + 'is not a function')
    }
    const A = Array(len)
    let k = 0
    while (k < len) {
        if (k in o) {
            const val = callback.call(thisObject, o[k], k, o)
            A[k] = val
        }
        k ++
    }
    return A
}
  1. reduce方法的实现
1. Let `O` be ? [ToObject](https://tc39.es/ecma262/#sec-toobject)(this value).
2. Let `len` be ? [LengthOfArrayLike](https://tc39.es/ecma262/#sec-lengthofarraylike)(`O`).
3. If [IsCallable](https://tc39.es/ecma262/#sec-iscallable)(`callbackfn`) is false, throw a TypeError exception.
4. If `len` = 0 and `initialValue` is not present, throwTypeError exception.
5. Let `k` be 0.
6. Let `accumulator` be undefined.
7. If `initialValue` is present, then
    a. Set `accumulator` to `initialValue`.
8. Else,
    a. Let `kPresent` be false.
    b. Repeat, while `kPresent` is false and `k` < `len`,
        i. Let `Pk` be ! [ToString](https://tc39.es/ecma262/#sec-tostring)([𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(`k`)).
        ii. Set `kPresent` to ? [HasProperty](https://tc39.es/ecma262/#sec-hasproperty)(`O`, `Pk`).
        iii. If `kPresent` is true, then
            1. Set `accumulator` to ? [Get](https://tc39.es/ecma262/#sec-get-o-p)(`O`, `Pk`).
        iv. Set `k` to `k` + 1.
     c. If `kPresent` is false, throwTypeError exception.
9. Repeat, while `k` < `len`,
    a. Let `Pk` be ! [ToString](https://tc39.es/ecma262/#sec-tostring)([𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(`k`)).
    b. Let `kPresent` be ? [HasProperty](https://tc39.es/ecma262/#sec-hasproperty)(`O`, `Pk`).
    c. If `kPresent` is true, then
        i. Let `kValue` be ? [Get](https://tc39.es/ecma262/#sec-get-o-p)(`O`, `Pk`).
        ii. Set `accumulator` to ? [Call](https://tc39.es/ecma262/#sec-call)(`callbackfn`, undefined, « `accumulator`, `kValue`, [𝔽](https://tc39.es/ecma262/#%F0%9D%94%BD)(`k`), `O` »).
    d. Set `k` to `k` + 1.
10. Return `accumulator`.
Array.prototype.reduce = function(callback, initialValue){
    const o = Object(this)
    const len = this.length >>> 0
    if (typeof callback !== 'function') {
        throw new TypeError(callback + 'is not a function')
    }
    if (len === 0 && !initialValue) {
        throw new TypeError('Reduce of empty array with no initial value')
    }
    let k = 0;
    var accumulator;
    if (initialValue) {
        accumulator = initialValue
    } else {
        let kPresent = false;
        while (!kPresent && k < len) {
            if (k in o) {
                accumulator = o[k]
                kPresent = true
            }
            k ++
        }
        if (!kPresent) {
            throw new TypeError('Reduce of empty array with no initial value')
        }
    }
    while(k < len) {
        if (k in o) {
            accumulator = callback(accumulator, o[k], k, o)
        }
        k++
    }
    return accumulator
}

··· 未完,更新中 ···