【JavaScript进阶2 ,附多道手撕~】

咱也可以看看咱之前写的JavaScript进阶1


1. JS的数据类型你了解多少?

1.1 概念

1.1.1 数据类型有以下八种
  • undefined
  • Null
  • Boolean
  • String
  • Number
  • Symbol
  • BigInt
  • Object
    • Array
    • RegExp
    • Date
    • Math
    • Function
1.1.2 数据类型的存储
  • 基础数据类型存储在栈内存中,被引用或拷贝时,会创建一个完全相等的变量
  • 引用类型会存储在堆内存中,存储的是地址,多个引用指向同一个地址,这里会涉及"共享"的概念

什么是共享?看下面两道例题

let a = {
    name: 'lee',
    age: 18
  }
let b = a;
console.log(a.name);
b.name = 'son';
console.log(a.name);
console.log(b.name);

这里打印 lee,son,son

我们可以发现 b 改变了 name , aname 也跟着改变了,这是因为他们都是引用数据类型,他们存储的都是堆内存中地址,指向的是同一个数据



let c = {
    name:'Julia',
    age: 20
  }

  function change(o) {
    o.age = 24;
    o = {
      name: 'Kath',
      age: 30
    }
    return o;
  }
  let d = change(c);
  console.log(d.age, d.name);
  console.log(c.age, c.name);

这里打印 30 Kath, 24 Julia

change 函数中 return o ; 相当于是改变了内存的地址,即新的一个地址,所以 d.age 指向的是 o

c.age 是因为最开始传入的 c , 和 change 函数中第一行代码 o 共享一个堆内存的数据,指向的是同一个地址,所以有一个改变了,另一个也会变。

请添加图片描述


总结

  • 所有引用数据类型存储的都是指向堆内存的地址
  • function 内直接改变,那么所有指向这个地址的数据都会跟着改变
  • return 一个新的数据,那么会建立一个新的内存地址去存储


1.2 数据检测

1.2.1 typeof
  • 优点:typeof 操作符是一种简单、快速的方式来判断基本数据类型。它返回的结果是一个字符串,可以直接用于条件判断。
  • 缺点:对于引用类型(除了function)的判断结果都是’object’,无法细分具体的引用类型。同时,对于null的判断结果也是object,不能准确判断null
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof undefined); // undefined
console.log(typeof true); // boolean
console.log(typeof Symbol()); //symbol
console.log(typeof null); // object
console.log(typeof []); // obejct
console.log(typeof {}); // object
console.log(typeof console); // object
console.log(typeof console.log); // function

可以看到前6个都是基本数据类型,但是第6个 null 却是 object , 这是js 的历史遗留问题,我们可以直接用 === 来判断是否为 null


1.2.2 instanceof(附手撕实现)

instanceofJavaScript 的一个操作符,用于检查某个对象是否是某个特定类的实例

  • 优点:instanceof 操作符可以判断一个对象是否是某个构造函数的实例 ,可以用于自定义构造函数的判断。它可以处理继承关系,如果对象是某个构造函数的子类实例,也会返回true
  • 缺点:instanceof 操作符只能判断对象是否是特定构造函数的实例,不能判断基本数据类型的数据。此外,如果在多个窗口或框架中使用,可能会导致不准确的结果。

示例

  let Car = function(){};
  let benz = new Car();
  console.log(benz instanceof Car); // true

  let car = new String('Mecredes Benz');
  console.log(car instanceof Car); // false

  let str = 'Covid-19';
  console.log(str instanceof String); // false
  // 这里说明不能判断基本数据类型,只能判断是否是构造函数的实例

手写实现 instanceof

 // 手撕instanceof
    function myInstanceof(left, right) {
      // 这里用typeof来判断是否是基础数据类型,如果是,直接返回false,注意null要单独处理
      if (typeof left !== 'object' || left === null) return false;

      //getPrototypeOf 是 Obejct对象带的API,可以拿到参数的原型对象
      let proto = Object.getPrototypeOf(left);
      // 循环往下找,直到找到相同的原型对象
      while (true) {
        if (proto === null) return false; // 没找到
        if (proto === right.prototype) return true; // 找到相同的原型对象
        proto = Object.getPrototypeOf(proto);
      }
    }


    // 测试 myInstanceof
    console.log('--------myInstanceof')
    let testInstanceof = function (){};
    testIns1 = new testInstanceof();
    console.log(myInstanceof(testIns1, testInstanceof));

    let testIns2 = new String('手撕Instanceof')
    console.log(myInstanceof(testIns2, testInstanceof));

    let testIns3 = 'asd';
    console.log(myInstanceof(testIns3, String));

请添加图片描述


1.2.3 Object.prototype.toString.call

每一个继承Object 的对象都有 toString 方法,如果 toString 方法没有重写的话,会返回 [Object type],其中 type 为对象的类型。但当除了 Object 类型的对象外,其他类型直接使用 toString 方法时,会直接返回都是内容的字符串,所以我们需要使用 call 或者apply方法来改变 toString 方法的执行上下文(不懂执行上下文可以点击看看这篇文章)。

示例

/*------ Object.prototype.toString -------*/
console.log('----------Object.prototype.toString');
console.log(Object.prototype.toString.call({}));
console.log(Object.prototype.toString.call(1));
console.log(Object.prototype.toString.call('1'));
console.log(Object.prototype.toString.call(true));
console.log(Object.prototype.toString.call(function(){}));
console.log(Object.prototype.toString.call(null));
console.log(Object.prototype.toString.call(undefined));
console.log(Object.prototype.toString.call(Symbol));
console.log(Object.prototype.toString.call(new Date()));
console.log(Object.prototype.toString.call(/123/g));
console.log(Object.prototype.toString.call([]));
console.log(Object.prototype.toString.call(document));
console.log(Object.prototype.toString.call(window));
console.log(Object.prototype.toString.call(NaN));

请添加图片描述

这个方法就很强大了,都可以判断。我们如果想就拿到后面的结果,可以用 slice 截取吧就像这样Object.prototype.toString.call(1).slice(7,-1)



1.3 数据类型转换

1.3.1 强制类型转换

首先咱先看看强制类型转换的方法都有啥子 (这里就前面两个面试喜欢问,当然还有一些比如字符串的加法规则啥的也有)

  • Number()

    • 布尔值,则返回1或0

    • 数字返回自身

    • null 返回0

    • undefined 返回NaN

    • 字符串

      • 字符串只包含数字,则转换为十进制
      • 字符串包含有效格式的浮点数,将其转化为浮点数值
      • 空字符串转化为0
      • 其它情况均返回NaN
    • Symbol 抛出错误

    • object 并且部署了[Symbol.toPrimitive] 方法,则会调用此方法,否则会调用对象的valueOf() 方法

      • Symbol.toPrimitive: 指将被调用的指定函数值的属性转换为相对应的原始值
    • 测试

       /*-----------  强制类型转换  -----------*/
          // Number()
          console.log('---Numebr()');
          console.log(Number(true)); // 1
          console.log(Number(false)); // 0
          console.log(Number('0111')); // 111
          console.log(Number(null)); // 0
          console.log(Number('')); // 0
          console.log(Number('la')); // NaN
          console.log(Number(-0X11)); // -17
          console.log(Number('0X11')); // 17
          console.log(Number('-0X11')); // NaN
      

      请添加图片描述

  • Boolean()

    • undefinednullfalse''0(包括+0和-0)NaN 转换出来都是 false
    • 其他的都是 true
    • 测试
    console.log('----Boolean()');
    console.log(Boolean(0)); // false
    console.log(Boolean(null)); // false
    console.log(Boolean(undefined)); // false
    console.log(Boolean(NaN));// false
    console.log(Boolean(1)); // true
    console.log(Boolean(13));// true
    console.log(Boolean('12'));// true
    

    请添加图片描述

  • parseInt()

    • 解析一个字符串,并返回一个整数
    • parseInt(String, radix) 其中第一个参数是待转换的字符串,第二个参数是要转换的进制,如果进制在 2--36 之外,则会返回NaN
    • 如果第一个参数为 null 或者为空字符串则会 NaN
    • 如果字符串不是表示 Number 类型的值,也会 NaN
    • 测试
        // parseInt()
        console.log('--------parseInt()');
        console.log(parseInt('0', 10)); // 0
        console.log(parseInt('-0', 10)); // -0
        console.log(parseInt('470', 10)); // 470
        console.log(parseInt('-FF', 16)); // -255
        console.log(parseInt('1100110', 2));// 102
        console.log(parseInt('Kona', 27)); // 411787
        console.log(parseInt('123', 1)); // NaN
        console.log(parseInt('', 10)); // NaN
    

    请添加图片描述

  • parseFloat()

    • parseFloatparseInt 的区别就是它是把字符串转化为浮点数,其他的规则一样。
  • toString() (深入剖析)

    • 这个看名字应该就知道是转化为 String 吧,第二次碰到了,还是写下来吧,本来在第二小节用 Object.prototype.toString.call 来判断数据类型就应该写的
    • 每个对象都有一个 toString()方法,我们可以通过 hasOwnProperty() 方法来判断toString 是不是该对象的自带属性。当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 [object type],其中 type 是对象的类型。
    • 不同类型对象的toString() 方法的返回值不同,因为他们都自己重写过,请看下面代码来证实
     // 剖析toString()
        console.log('-------剖析toString()')
        // 首先看看这些数据类型,toString是不是他们自身属性
        console.log(Number.prototype.hasOwnProperty('toString')); // 全为true
        console.log(Object.prototype.hasOwnProperty('toString'));
        console.log(String.prototype.hasOwnProperty('toString'));
        console.log(Array.prototype.hasOwnProperty('toString'));
        console.log(Date.prototype.hasOwnProperty('toString'));
        console.log(RegExp.prototype.hasOwnProperty('toString'));
        console.log(Function.prototype.hasOwnProperty('toString'));
    
        // 然后再验证不同的构造函数生成的对象,其toString返回值不同
        console.log('然后再验证不同的构造函数生成的对象,其toString返回值不同');
        let num = new Number('123asd');
        console.log(num.toString()); // NaN
    
        let str = new String('123asd');
        console.log(str.toString()); // '123asd'
    
        let bool = new Boolean('123asd');
        console.log(bool.toString()); // true
    
        let arr = new Array(1,2);
        console.log(arr.toString()); // '1,2'
    
        let date = new Date();
        console.log(date.toString()); // xxx时间
    
        let fn = function() {};
        console.log(fn.toString()); // function(){}
    
        // 下面两个就会直接返回对象类型,即没有封装自己toString
        let obj = new Object({});
        console.log(obj.toString()); // [obejct Object]
        console.log(Math.toString()); // [object Math]
    

    请添加图片描述

    • 所以之前用 Object.prototype.toString 就要加上 .call() 方法改变他的执行上下文,此外,我们直接对那些封装了自己的 toString 方法的数据类型,譬如 Number.toString() 会输出一个函数
  • String()

    • 至于这个方法很明显就是把其它类型转化为 String 类型,没有什么要注意的,不过对于对象,是返回[object Obeject] ,测试如下
    // String()
        console.log('----String()')
        console.log(String(null)); // null
        console.log(String(undefined)); // undefined
        console.log(String(true)); // true
        console.log(String(false)); // false
        console.log(String(Symbol)); // function Symbol() { [native code] }
        console.log(String(Object)); // function Object() { [native code] }
        console.log(String([1,2,3])); // 1,2,3
        console.log(String({id:1,name:2,age:3})); // [object Object]
        console.log(String(1)); // 1
        console.log(String('1')); // 1
    

    请添加图片描述


1.3.2 隐式类型转换
  • == 隐式类型转换规则
    • 如果类型相同,则无需进行类型转换
    • 如果其中一个操作值为 nullundefined ,那么另一个操作符必须为nullundefined 才为 true , 否则为 false
    • 如果其中一个为 Symbol 类型, 则返回 false
    • 如果两个操作值一个为 String ,一个为 Number , 则会转化为 Number
    • 如果一个操作值是boolean 则会转化为 Number
    • 如果一个操作值为Object, 且另一个为 String、Number、Symbol就会把Object 转化为原始类型再判断
  • + 隐式类型转换规则
    • 如果一个为Number , 一个为 String ,则会把 Number 转化为 String ,再进行字符串拼接
    • 如果一个是String , 另外一个是 undefined 、 null 、 Boolean ,则调用toString 方法进行字符串拼接
    • 如果一个是Number ,另一个是 undefined 、 null 、 Boolean ,则会转化为数字再进行加法运算
/*----------- 隐式类型转换  ------------*/
console.log('-------隐式类型转换');
console.log('abc' + 123 + 5); // abc1235
console.log(123 + 5 + 'abc'); // 128abc
console.log(undefined + '111'); // undefined111
console.log('111' + null); // 111null
console.log('abc' + Boolean); //abcfunction Boolean() { [native code] }
console.log(123 + undefined); // NaN
console.log(null + 123); // 123
console.log(true + 123); // 124
console.log(123 + Boolean); //123function Boolean() { [native code] }



2. 浅拷贝与深拷贝

2.1 浅拷贝

自己创建一个新的对象,来接受你要重新赋值或引用的对象值。如果对象属性是基本数据类型,复制的就是基本数据类型的值给新对象;但如果是引用数据类型,复制的就是内存中的地址,如果其中一个对象改变了这个内存的地址,就会影响到其它同样指向这个地址的对象。

简单地说,浅拷贝就是,当是基本数据类型时候,就是复制了他的值

但是如果是引用数据类型的时候,复制的是内存地址,也就是 1.1.2 数据类型的存储 中的共享概念

但是它影响只会影响最表层那一块。譬如下面这个例子

let obj = {a:1, b: {c: 1}};
let obj2 = {...obj};
console.log('obj', obj); // 打印obj {a:2, b: {c: 2}}
console.log('obj2', obj2); // 打印obj2 {a:1, b: {c: 2}};
obj.a = 2;
obj.b.c = 2;
2.1.1 object.assign

es6的新方法,可以用于多个对象的合并,也就可以用来进行浅拷贝

  • 它不会拷贝对象的继承属性
  • 不会拷贝对象的不可枚举属性
  • 可以拷贝Symbol 类型的属性

示例一

let target = {};
let source = {a: {b: 1}};

Object.assign(target, source);
console.log(target); // {a: {b: 10}}

source.a.b = 10;
console.log(source); // {a: {b: 10}}
console.log(target); // {a: {b: 10}}

示例二,测试不能拷贝不可枚举属性和可以拷贝 Symbol 类型属性

这里不知道 Object.defineProperty 的可以看看这篇文章Object.defineProperty 详解 , Vue响应式就是通过这个方法来实现的

let obj1 = {a: {b: 1}, sym: Symbol(1)};
Object.defineProperty(obj1, 'name', {
  value: '不可枚举属性',
  enumerable: false, // 设置不能遍历
});

let obj2 = {};
Object.assign(obj2, obj1);
obj1.a.b = 2;
console.log('obj1', obj1); 
console.log('obj2', obj2); // name属性没有拷贝过来,因为设置了enumerable: false

请添加图片描述


2.1.2 …扩展运算符进行浅拷贝
console.log('---扩展运算符')
let obj3 = {a:1, b: {c: 1}};
let obj4 = {...obj3};
obj3.a = 2;
console.log(obj3);
obj3.b.c = 2;
console.log(obj4);

let arr1 = [1,2,3];
let newArr1 = [...arr1];
console.log(newArr1);

请添加图片描述

2.1.3 手撕浅拷贝
function shallowClone(target) {
  // 首先判断是否为引用类型,如果不是的话则直接返回
  if(typeof target === 'object' && target !== null) {
    // 判断是数组还是对象,进行初始化定义
    let cloneTarget = Array.isArray(target)? [] : {};
    for(let prop in target) {
      // 只复制表层的属性值,即target自带的属性值
      if(target.hasOwnProperty(prop)) {
        cloneTarget[prop] = target[prop];
      }
    }
    return cloneTarget;
  }else {
    // 不是引用类型直接返回
    return target;
  }
}

测试手写的浅拷贝

let obj = {a:1, b: {c: 1}};
let obj2 = {};
obj2 = shallowClone(obj);
console.log('obj', obj); // 打印obj {a:2, b: {c: 2}}
console.log('obj2', obj2); // 打印obj2 {a:1, b: {c: 2}}
obj.a = 2;
obj.b.c = 2;

2.2 深拷贝

对于复杂引用类型,其在堆内存中完全开辟一块新的内存地址,将原有对象完整的复制过来,修改并不会对原有对象造成影响。

2.2.1 JSON.parse(JSON.stringify())

最简单的深拷贝方法就是 JSON.parse(JSON.stringify())

原理就是先用 JSON.stringify 将其转换为单纯地 JSON 对象,而后再将其转化为 JS 的对象,这样就会新创建一个堆内存地址存放新的 JS 对象。

使用 JSON.parse(JSON.stringify()) 进行深拷贝的注意事项

  • 拷贝对象的值如果有 functionundefinedsymbol 这几种类型,经过 JSON.stringify 序列化后的字符串中,这个键值对会消失
  • 拷贝 Date 引用类型会变成字符串
  • 无法拷贝不可枚举的属性
  • 无法拷贝对象的原型链
  • 拷贝 RegExp 引用类型会变成空对象
  • 对象中含有NaN±InfinityJSON 序列化结果会变成 null
  • 无法拷贝对象的循环应用,即对象成环现象(obj[key] = obj

测试

 /*------ JSON.parse(JSON.stringify()) --------*/
let obj1 = {a:1, b: [1,2,3], c: undefined, d: () => {console.log('d')}, e: Symbol(1)};
Object.defineProperty(obj1, 'name', {
  value: '不可枚举属性',
  enumerable: false
})

let obj2 = JSON.parse(JSON.stringify(obj1));
console.log('obj2',obj2);
obj1.a = 2;
obj1.b.push(4);
console.log('obj1', obj1);
console.log('obj2', obj2);
// 打印结果如下图

请添加图片描述


2.2.2 手撕深拷贝(简陋版)
/*---------- 手撕深拷贝V1.0 -----------*/
function deepClone1(obj) {
  let cloneObj = {};
  for(let key in obj) {
    // 如果是引用数据类型,则递归调用
    if(typeof obj[key] === 'object' && obj[key] !== null) {
      cloneObj[key] = deepClone1(obj[key])
    }else {
      cloneObj[key] = obj[key];
    }
  }
  return cloneObj;
}

// 测试
let obj1 = {
  a: {
    b: 1,
  },
  b: 1,
  c: undefined,
  reg: /123/,
  NaN: NaN,
  infinity: Infinity,
  sym: Symbol(1),
  Null: null,
};
Object.defineProperty(obj1, 'name', {
  value: 'aaa',
  enumerable: false
})
let obj2 = deepClone1(obj1);
obj1.a.b = 2;
obj1.b = 2;
console.log(obj1,'obj1');
console.log(obj2,'obj2');
//打印结果如下

请添加图片描述

我们可以发现RegExp 对象还是没有拷贝过来,除此之外,name 属性不可枚举也没有拷贝过来


2.2.3 手撕深拷贝(改进版)

针对简陋版的缺陷我们可以用以下方法解决

  • 针对能够遍历对象的不可枚举属性,我们可以用 Reflect.ownKeys() 方法获取一个由目标对象自身的属性键组成的数组。Reflect.ownKeys(target) 的返回值等于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
  • 当参数为 RegExpDate 类型时,则直接生成一个新的实例返回
  • Object.getOwnPropertyDescriptors() 获得对象的所有属性,以及对应的特性,结合Object.create() 创建一个新对象,并继承传入原对象的原型链
  • WeakMap 类型作为 Hash 表, 因为 WeakMap弱引用类型 ,可以有效防止内存泄漏,作为检测循环引用很有帮助,如果存在循环,则引用直接返回WeakMap 存储的值,并且不影响垃圾回收机制,避免造成内存泄漏,提高性能。

手撕深拷贝

// 判断是否为复杂引用类型
const isComplexDataType = obj => (typeof obj === 'object' && obj !== null);

const deepClone = (obj, hash = new WeakMap()) => {
  // 如果是Date或RegExp类型直接返回新对象
  if(obj.constructor === Date) {
    return new Date(obj);
  }
  if(obj.constructor === RegExp) {
    return new RegExp(obj);
  }

  // 如果存在循环引用,即存在obj[key] = obj;用WeakMap解决
  if(hash.has(obj)) {
    return hash.get(obj);
  }


  let allDesc = Object.getOwnPropertyDescriptors(obj); // 获取obj自身的所有属性及其特性
  // 创建新对象cloneObj,并继承obj自身的原型,并传入obj自身的所有属性和属性的特性
  let cloneObj = Object.create(Object.getPrototypeOf(obj), allDesc);


  // 递归继承其原型链
  hash.set(obj, cloneObj);  // 以键为obj,值为cloneObj,通过递归不断更新
  for(let key of Reflect.ownKeys(obj)) {
    // 如果是复杂引用类型则递归调用
    cloneObj[key] = isComplexDataType(obj[key]) ? deepClone(obj[key], hash) : obj[key];
  }

  return cloneObj;
}

测试
请添加图片描述

// 验证深拷贝V2.0
console.log('----手撕深拷贝V2.0')
let obj = {
  num: 0,
  str: '',
  boolean: true,
  unf: undefined,
  nul: null,
  obj: { name: '一个对象', value: 1 },
  arr: [0, 1, 2],
  fn: function () { console.log('我是一个函数') },
  date: new Date(0),
  reg: new RegExp('/我是一个正则/ig'),
  [Symbol('1')]: 1
};

// 加入aaa属性,设置为不可枚举
Object.defineProperty(obj, 'aaa', {
  value: '不可枚举属性',
  enumerable: false
});

// 设置循环引用属性loop
obj = Object.create(obj, Object.getOwnPropertyDescriptors(obj));
obj.loop = obj;

// 测试
let cloneObj = deepClone(obj);
cloneObj.arr.push(4);
console.log('obj', obj);
console.log('cloneObj', cloneObj);



3. JS的继承你了解多少?

不妨先来看两个面试题

  1. JavaScript 的继承有多少种方式实现?

我目前只想到了call/apply/bind ,然后深拷贝,然后还可以用Object.create 实现继承吧

  1. ES6extends 关键字是用哪种继承方式实现的?

不知道啊啊

等咱们学完了再来看这两个问题吧啊

3.1 概念

继承主要是用构造函数以及原型链实现继承效果。它可以让子类对象(实例)使用父类的所有属性以及方法,并且可以直接在子类上扩展新的属性或方法。

使用继承可以提高我们代码的复用性,从而减少我们的代码量,降低开发成本。


3.2 7种继承方法

3.2.1 原型链继承
  • 每一个构造函数都有一个原型对象
  • 原型对象又包含一个指向构造函数的指针
  • 而实例则包含一个原型对象的指针
 /*--------- 1. 原型链继承 ---------*/
    function Parent1() {
      this.name = 'parent1';
      this.play = [1, 2, 3];
    }
    function Child1() {
      this.type = 'child1';
    }

    Child1.prototype = new Parent1();
	Child1.prototype.constructor = Child1;
    console.log(new Child1());

    // 缺点 就是共享问题,指向同一个堆内存地址,一个变其它都会变
    let child1 = new Child1();
    let child2 = new Child1();
    child1.play.push(4);
    console.log('child1', child1); // 数组全部变成了[1, 2, 3, 4];
    console.log('child2', child2);

请添加图片描述


3.2.2 构造函数继承

他是父类的引用属性,不会被共享,解决了第一种方法的弊端

但是只能继承实例的属性和方法,不能继承原型的属性和方法

/*----------- 2. 构造函数继承 ---------*/
function Parent2() {
  this.name = 'parent2';
}

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

function Child2() {
  Parent2.call(this);
  this.type = 'child2';
}

let child2 = new Child2();
console.log('child2', child2);
console.log(child2.getName()); // 报错

请添加图片描述


3.2.3 组合继承

结合前两种继承方法的优点,就有了这种继承方法

这种方法解决了上面两种方法的缺陷

但是调用了两次 Parent3 ,进行两次构造,一次是指定child3.prototype 时候, 还一次是通过 call 调用改变 this 指向的时候, 多构造一次就多进行了一次性能开销,还有更好的办法吗?

console.log('-------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
Child3.prototype = new Parent3();
// 手动挂上构造器,指向自己构造函数
Child3.prototype.constructor = Child3;

// 测试组合继承
let child3_1 = new Child3();
let child3_2 = new Child3();

child3_1.play.push(4);
console.log(child3_1.play, child3_2.play); // 不互相影响
console.log(child3_1.getName()); // 正常使用原型上的函数

请添加图片描述


3.2.4 Object.create 原型式继承

嘿,我就说她也可以吧

/*----------- 4. 原型式继承 ---------*/
console.log('-------4. 原型式继承')
let Parent4 =  {
  name: 'parent4',
  play: [1, 2, 3],
  getName: function() {
    return this.name;
  }
}
let child4_1 = Object.create(Parent4);
let child4_2 = Object.create(Parent4);
child4_1.name = 'child4_1';
child4_1.play.push(4);

child4_2.name = 'child4_2';
child4_2.play.unshift(4);

console.log('child4_1', child4_1);
console.log('child4_2', child4_2);
console.log(child4_1.getName());

请添加图片描述

看打印可以发现,多个实例引用指向相同的内存,存在篡改的可能。


3.2.5 寄生式继承

在原型式继承的基础上进行优化

使用原型是继承获取一份目标对象的浅拷贝,然后利用这个浅拷贝在父类上增加一些方法。

/*----------- 5. 寄生式继承 ---------*/
console.log('-------5. 寄生式继承')
let Parent5 =  {
  name: 'parent5',
  play: [1, 2, 3],
  getName: function() {
    return this.name;
  }
}

function clone(original) {
  let clone = Object.create(original);
  clone.getPlay = function() {
    return this.play;
  }
  return clone;
}

// 测试
let child5 = clone(Parent5);

console.log(child5.getName());
console.log(child5.getPlay());

请添加图片描述


3.2.6 寄生组合式继承

结合前面的方法的优缺点得出这种继承方法,这也是目前最优的继承方式

/*----------- 6. 寄生组合式继承 ---------*/
console.log('--------6. 寄生组合式继承')
function clone2(parent, child) {
  // 这里改用Object.create 就可以减少组合继承中多进行一次Parent3构造的过程
  child.prototype = Object.create(parent.prototype);
  child.prototype.constructor = child;
}

function Parent6() {
  this.name = 'parent6';
  this.play = [1, 2, 3];
}

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

function Child6() {
  Parent6.call(this);
  type:'child6';
  this.friends = 'child6';
}
clone2(Parent6, Child6); // 继承
Child6.prototype.getFriends = function() {
  return this.friends;
}

// 测试
let child6_1 = new Child6();
let child6_2 = new Child6();
child6_1.play.push(4);
child6_2.play.unshift(4);
console.log('child6_1', child6_1);
console.log('child6_2', child6_2);
console.log(child6_1.getName());
console.log(child6_1.getFriends());

请添加图片描述


3.2.7 ES6的extends关键字实现继承
/*----------- 7. extends实现继承 ---------*/
console.log('------7. extends实现继承')
class Person {
  constructor(name) {
    this.name = name;
  }
  getName = function() {
  console.log('Person: ', this.name);
  }
  play = [1, 2, 3];
}

class Gamer extends Person {
  constructor(name, age) {
    // 子类中存在构造函数,则需要在使用 this 之前先调用 super();
    super(name);
    this.age = age;
  }
}

let a = new Gamer('A', 20);
let b = new Gamer('B', 22);
a.getName();
a.play.push(4);
b.play.pop();
console.log('a', a);
console.log('b', b);

请添加图片描述

这里需要注意,当浏览器不兼容 ES6 时候我们要做向下兼容,比如IE8/9 就不支持。可以用babel


继承记忆图

我觉得还可以看看这个深入理解JavaScript——继承

请添加图片描述


3.3 手撕New、 apply & bind &call

不妨先思考下面三个问题

  1. 怎么实现 new 关键字?
  2. apply、call、bind 三者有什么区别?
  3. 怎么实现 apply 或者 call 方法
3.3.1 new 原理介绍

new 也被称为隐式原型继承,这个关键字的主要作用如下

  1. 在内存中创建一个新对象
  2. 将新对象的 [[Prototype]]__proto__ 被赋值为构造函数的prototype 属性
  3. 将构造函数中的 this 指向新对象
  4. 执行构造函数中的代码
  5. 如果构造函数返回非空对象,则返回该对象;否则返回创建的新对象。

我们先来看看一个常见的 new 的例子

function Person() {
  this.name = 'Jack';
}
let p = new Person();
console.log(p.name);

需要注意的是第一条,当复杂引用类型返回的是一个非空对象,那么 new 返回的就是那个非空对象,而不会执行构造函数里的代码,如果返回的不是对象,那么还是执行构造函数里的代码,也就是接收的是新创建的对象。

比如说说当 Person() 里面返回一个 return {age: 30}; 那么p.name 就是 undefined


3.3.2 手撕new(2种方法)

回顾一下 new 被调用后大致做了哪几件事情?

  1. 在内存中创建一个新对象
  2. 将新对象的 [[Prototype]]__proto__ 被赋值为构造函数的prototype 属性
  3. 将构造函数中的 this 指向新对象
  4. 执行构造函数中的代码
  5. 如果构造函数返回非空对象,则返回该对象;否则返回创建的新对象。
/*----- new原理 -------*/
    // 简单的new使用例子
    function Person() {
      this.name = 'Jack';
    }
    let p = new Person();
    console.log(p.name);

    /*----- 手撕new ------*/
    function _new(Constructor, ...args) {
      if (typeof Constructor !== 'function') {
        throw 'Constructor must be a function';
      }

      // 1. 创建一个新对象
      let obj = Object.create(null);
      // 2. 将新对象的 `[[Prototype]]` 特性被赋值为构造函数的 `prototype` 属性
      obj.__proto__ = Constructor.prototype;
      // 3. 构造函数内部的 `this` 被赋值给这个新对象
      // 4. 执行构造函数内部的代码
      let result = Constructor.apply(obj, args);
      // 5. 如果构造函数返回非空对象, 则返回该对象, 否则返回新对象
      return (typeof result === 'object' || typeof result === 'function') ? result : obj;
    }

    // 测试
    console.log('测试手写的new');
    let p2 = _new(Person,);
    console.log(p2.name);

    /*---------  手撕new2 -----------*/
    // ES5之前没有 Object.create,要怎么实现new?
    function _new2() {
      // 基于new Object 创建实例。
      let obj = new Object();
      // 获取外部的构造器
      Constructor = Array.prototype.shift.call(arguments);
      // 手写Object.create()的核心
      let F = function () { };
      F.prototype = Constructor.prototype;
      // 指向正确的原型
      obj = new F();
      // 借用外部传入的构造函数给obj设置属性
      let result = Constructor.apply(obj, arguments);

      //执行结果如果返回非空对象, 则返回该对象, 否则返回新对象
      return (typeof result === 'object' || typeof result === 'function') ? result : obj;
    }

    console.log('测试不用Object.create写出来的new')
    let p3 = _new2(Person);
    console.log(p3.name);

请添加图片描述


3.3.3 apply & call & bind 原理介绍

call、apply、bind 是挂在 Function 对象上的三个方法,调用这三个方法必须是一个函数, 他们都可以改变函数的 this 指向

其中 call、apply 的区别是传参写法不同,call 是从第 2 个到第 n 个就是在传参,而 apply 第二个是一个数组,参数放数组里面

bind 与前两者的区别就是,虽然它也改变 this 指向,但是它不是立即执行的,而前两者是立即执行的。

他们的基本语法如下

func.call(thisArg, param1, param2, ...);
func.apply(thisArg, [param1, param2, ...]);
func.bind(thisArg, param1, param2, ...);

使用示例

let a = {
  name: 'jack',
  getName: function(msg) {
    return msg + this.name;
  }
}
let b = {
  name: 'lily'
}
console.log(a.getName('hello~')); // hello~jack
console.log(a.getName.call(b, 'hi~')); // hi~lily
console.log(a.getName.apply(b, ['hi~'])); // hi~lily
let name = a.getName.bind(b, 'hello~');
console.log(name()); // hello~lily

3.3.4 手撕 apply & call & bind

我们先来想想 apply 和 call 的注意点

  1. 改变 this 指向
  2. 直接在 Function 原型上调用得到 undefined ,比如Function.prototype.call(xx) 得到的是 undefined ,必须要有实例。
  3. 考虑 callnull 的情况,那么那时候 this 指向 window
  4. applycall 的区别就是参数传递方式的不同,他们都是返回函数执行得到的结果,而 bind 返回的是待执行的函数。所以在 call和apply 里我们是首先改变函数的 this 指向,然后得到函数执行的结果, 并返回执行结果

手撕 apply & call

Function.prototype.myCall = function(context, ...args) {
  if(this === Function.prototype) {
    return undefined; // 防止Function.prototype.mycall 直接调用
    // 譬如Function.prototype.call(a); // undefined
  }
  var context = context || window; // 考虑fn.call(null)的情况,如果是null则指向window,防御性代码

  context.fn = this; // 改变函数的this指向
  let result = context.fn(...args) // 立即执行函数并得到结果
  delete context.fn // 删除context属性,释放内存,并返回结果result
  return result;
}

Function.prototype.myApply = function(context = window, args) {
  if(this === Function.prototype) {
    return undefined;
  }
  context.fn = this;
  let result = context.fn(...args);
  delete context.fn;
  return result;
}

 // 测试手撕
console.log('---测试myCall---');
console.log(a.getName.myCall(b, 'hi~'));
console.log('----测试myApply-----');
console.log(a.getName.myApply(b, ['hello~']));

请添加图片描述


手撕 bind

  1. 首先看到 bind 是返回一个函数,并且返回的函数可以接受参数,说明它是一个闭包。
  2. 其次,bind 不能原型方法调用,否则就会报错,所以我们根据这个特性,分为两种情况,如果 this instanceof Fntrue , 那么说明它是构造函数调用,如果是,我们就要用 new 创建一个新对象来调用函数,如果不是则 apply 出来 context
Function.prototype.myBind = function(context, ...args1) {
  if(this === Function.prototype) {
    throw new TypeError('Error');
  }
  const _this = this;
  return function F(...args2) {
    if(this instanceof F) { // 判断是否为构造函数
      // 如果是构造函数,则用最初的fn为构造器调用函数
      return new _this(...args1, ...args2);
    }
    // 如果不是构造函数,则用apply指向代码
    return _this.apply(context, args1.concat(args2));
  }
}


console.log('----测试myBind----');
let myBindName = a.getName.myBind(b, 'hello~');
console.log(myBindName());

请添加图片描述




4. this

4.1 this的4种绑定方式

4.1.1 默认绑定

直接在全局范围内调用函数,this 指向 Windows 全局

console.log('----默认绑定')
function girl() {
  console.log(this);
}
girl();
// 打印window对象

4.1.2 隐式绑定

调用一个对象的方法时,会出现隐式绑定,this 此时就会指向方法

console.log('----隐式绑定')
let girl2 = {
  name: '小红',
  height: 160,
  weight: 110,
  detail: function () {
    console.log('姓名:' + this.name);
    console.log('身高:' + this.height);
    console.log('体重:' + this.weight);
  }
}
girl2.detail();
/*打印
----隐式绑定
 姓名:小红
 身高:160
 体重:110
*/

4.1.3 硬绑定

使用 call、apply 方法改变 this 指向

/*---------------  硬绑定  ---------------*/
    // 使用call、apply方法改变this指向
    console.log('----硬绑定')
    let girlName = {
      name: '小红',
      sayName: function () {
        console.log('我的女朋友是' + this.name);
      }
    }

    let girlName1 = {
      name: '小白'
    }

    let girlName2 = {
      name: '小黄'
    }
    console.log('----call')
    girlName.sayName.call(girlName1);
    girlName.sayName.call(girlName2);
    console.log('----apply')
    girlName.sayName.apply(girlName1);
    girlName.sayName.apply(girlName2);
/* 打印
----硬绑定
 ----call
 我的女朋友是小白
 我的女朋友是小黄
 ----apply
 我的女朋友是小白
 我的女朋友是小黄
*/

4.1.4 构造函数绑定
console.log('----构造函数绑定')
function Lover(name) {
  this.name = name;
  this.sayName = function() {
    console.log('我的老婆是' + this.name);
  }
}

let name = '小白';
let xiaoHong = new Lover('小红');
xiaoHong.sayName();
/* 打印
----构造函数绑定
我的老婆是小红
*/

4.2 this指向相关3道练习题

先想想这三道题分别打印什么,答案在后面

4.2.1 第一道
function x() {
  function y() {
    console.log(this);
    function z() {
      "use strict";
      console.log(this);
    }
    z();
  }
  y();
}
x();

4.2.2 第二道
let names = "小白";
function special() {
  console.log('姓名', this.names);
}

let girl = {
  names: '小红',
  detail: function() {
    console.log('姓名', this.names);
  },
  women: {
    names: '小黄',
    detail:function() {
      console.log('姓名', this.names);
    }
  },
  special:special,
}

girl.detail();
girl.women.detail();
girl.special(); 

4.2.3 第三道
var name = '小红';
function a() {
  var name = '小白';
  console.log(this.name);
}
function d(i) {
  return i();
}
var b = {
  name: '小黄',
  detail:function() {
    console.log(this.name);
  },
  bibi: function() {
    return function() {
      console.log(this.name);
    }
  }
}

var c = b.detail;
b.a = a;
var e = b.bibi();
a();
c(); 
b.a();
d(b.detail); 
e();

4.2.4 答案

请添加图片描述

解析

  • 第一题
    • 首先直接在全局调用函数x(),那么 this 是默认指向,即指向全局,所以 y() 打印window 对象;
    • 第二个之所以为 undefined ,是因为开启了严格模式 use strict , 严格模式下, 事件处理函数内的 thisundefined
  • 第二题
    • 前面的小红、小黄应该没问题,都是调用对象内的函数,属于隐式绑定。
    • 第三个之所以为小红,是因为是在 girl 对象的基础上调用的 special() 方法,那么 this 就会指向真正调用它的对象,即指向 girl , 所以打印小红。
  • 第三题
    • 先看第一个直接在全局调用函数a() , this 默认指向 window , 所以第一个打印小红
    • 第二个全局调用函数 c() , 虽然 c 是 等于 b.detail 函数 , 但是实际调用者是 c ,而 cthis 指向全局,所以还是小红 (b.detail和b.detail() 的区别是前者是一个函数,后者是函数的执行结果)
    • 第三个 b.a() ,同上,实际调用者是 b ,所以 this 指向 b, 所以打印小黄
    • 第四个直接全局调用函数 d() ,而d() 指向全局,所以同样打印小红
    • 第五个和第二个一样,b.bibi() 返回的是一个匿名函数, 也就是 e = function(){...} ,但是实际调用者是 e , 而 e 是指向全局的,所以还是小红。



5. 手撕 JSON.stringify

JSON.stringifyjs 对象转化成 JSON 对象

JSON.parseJSON 对象转化成JS 的值或对象

最常见的比如复杂引用类型的深拷贝的应用 JSON.parse(JSON.stringify(obj)) , 原理就是先通过前者将其转化为 JSON 对象,而后再转换成一个新的 JS 对象, 这样就会为其分配新的堆内存。

JSON.parse 有两个参数,第一个是 JSON 对象,第二个是可选参数是一个函数可以看看下面的例子

const json = '{"result": true, "count": 2}';
const obj = JSON.parse(json);

console.log(obj.result, obj.count); // true 2

// 带第二个参数的情况
let result = JSON.parse('{"p": 5}', function (k, v) {
  if(k === '') return v;
  return v*2;
});
console.log(result); // {p: 10}

请添加图片描述


JSON.stringify 有三个参数,第一个是待转换的对象,第二个是replacer 函数,第三个参数用来控制结果字符串里面的间距。

手撕 JSON.stringify

function jsonStringify(data) {
  let type = typeof data;

  if(type !== 'object') {
    let result = data;
    // data可能是基础数据类型的情况处理
    if(Number.isNaN(data) || data === Infinity) {
      result = "null"; // NaN 和 Infinity序列化返回null
    }else if(type === 'function' || type === 'undefined' || type === 'symbol') {
      // 由于function 和 symbol序列化后返回undefined ,所以一起处理
      return undefined;
    }else if (type === 'string') {
      result = '"' + data + '"';
    }
    return String(result);
  }else if(type === 'object') {
    if(data === null) {
      return "null";
    }else if(data.toJSON && typeof data.toJSON === 'function') {
      return jsonStringify(data.toJSON());
    }else if(data instanceof Array) {
      let result = [];
      data.forEach((item, index) => {
        if(typeof item === 'undefined' || typeof item === 'function' || typeof item === 'symbol') {
          result[index] = "null";
        }else {
          result[index] = jsonStringify(item);
        }
      });
      result = "[" + result + "]";
      return result.replace(/'/g, "");
    }else {
      // 普通对象
      let result = [];
      Object.keys(data).forEach((item,index) => {
        if(typeof item !== 'symbol') {
          // 如果key是symbol对象,忽略
          if(data[item] !== undefined && typeof data[item] !== 'function' && typeof data[item] !== 'symbol') {
            // 键值如果是undefined、function、symbol为属性值时候,忽略
            result.push('"' + item + '"' + ":" + jsonStringify(data[item]));
          }
        }
      });
      return ("{" + result + "}").replace(/'/g, "");
    }
  }
}

测试

console.log('----测试jsonStringify------');
let obj2 = {
  age: 18,
  arr: ['asd', 123],
  sym: Symbol(2),
  fn: function(){
    console.log('aa');
  },
  info: {
    son: 'aaa',
    age: 2,
    xx: null,
    unf: undefined,
    inf: Infinity
  }
}

console.log(jsonStringify(obj2) === JSON.stringify(obj2));
console.log(jsonStringify(obj2));
console.log(JSON.stringify(obj2));

请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值