JS高阶(四)面向对象

324 阅读27分钟

1、面向对象基础

面向对象:object oriented programming OOP编程/设计思想

  • JS
  • JAVA
  • Python
  • PHP
  • C#
  • C++
  • GO
  • ....

面向过程:procedure oriented programming POP

  • C语言

面向对象是基于类和实例来管理一门语言的:

  • 在JS中所有要学习和研究的内容都可以被成为“对象”,JS把其划分为很多类,我们平时就是创造这些类的实例进行开发的;
  • 每个实例之间有一些自己的私有属性和方法,也具备类赋予他们的公共属性和方法;
  • JS语言本身就是基于“面向对象”这种思想设计的语言,所以我们学习和开发也应该按照这种思想来进行处理的!

JS中的内置类

  • 数据类型所属内置类

    • Number 数字类 每一个数字都是他的一个实例[含NaN]
    • String
    • Boolean
    • Symbol
    • BigInt
    • Object
    • Function
    • Array
    • RegExp
    • Date
    • Error
    • ....
  • DOM元素对象也有自己所有的内置类

    • 每一种HTML标签都有自己所属的内置类:HTMLDivElement / HTMLAnchorElement ... -> HTMLElement -> Element -> Node -> EventTarget -> Object
    • document实例 -> HTMLDocument「和 XMLDocument」 -> Document -> Node ...
    • window实例 -> Window -> WindowProperties -> EventTarget -> Object
    • 元素集合(document.getElementByTagName("*")) -> HTMLCollection -> Object
    • 节点集合 -> NodeList -> Object

JS中的自定义类:自己创建的类「“类”有一个专业的名字“构造函数”」

普通函数执行 VS 构造函数执行

const Fn = function Fn(x, y) {
    let sum = 10;
    this.total = x + y;
    this.say = function() {
        console.log(`我计算的和是:${this.total}`);
    };
};
let res1 = Fn(10, 20); //普通函数执行
console.log(res1); //undefined
console.log(window); //window.total、window.say
Snipaste_2021-08-09_14-23-36
let f1 = new Fn(10, 20); //构造函数执行「面向对象思维处理:创造类和实例 基于NEW执行,Fn是类,f1是实例」
console.log(f1);
let f2 = new Fn(100, 200);
console.log(f2);
console.log(f1.say === f2.say); //false 构造函数体中 this.xxx=xxx 给实例设置的私有属性方法
function Fn(x, y) {
    let sum = 10;
    this.total = x + y;
    this.say = function () {
        console.log(`我计算的和是:${this.total}`);
    };
}
let res = Fn(10, 20); //普通函数执行  Fn执行的返回值赋值给res
let f1 = new Fn(10, 20); //构造函数执行  把Fn当做一个类,f1是创造出来的一个实例
let f2 = new Fn;
console.log(f1.sum);
console.log(f1.total);
console.log(f1.say === f2.say);

微信图片_20211204001404

函数区别:

Fn() //普通函数执行

Fn; //代表函数Fn的堆内存地址 没执行

let f1 = new Fn(10, 20); //把构造函数执行,可以传递实参 有参数列表NEW 优先级:20

let f2 = new Fn; //不加小括号也可以执行的,只不过不允许传递实参 优先级:19

console.log(f1, f2);


2、prototype和__proto__

大部分“函数数据类型”的值都具备“prototype(原型/显式原型)”属性,属性值本身是一个对象「浏览器会默认为其开辟一个堆内存,用来存储实例可调用的公共的属性和方法」,在浏览器默认开辟的这个堆内存中「原型对象」有一个默认的属性“constructor(构造函数/构造器)”,属性值是当前函数/类本身!!

  • 函数数据类型
    • 普通函数(实名或匿名函数)
    • 箭头函数
    • 构造函数/类【内置类/自定义类】
    • 生成构造器 Generator
    • ....
  • 不具备prototype的函数
    • 箭头函数
    • 基于ES6给对象某个成员赋值函数值的快捷操作
    • ....
微信图片_20210810001829

每一个“对象数据类型”的值都具备一个属性“_proto_(原型链/隐式原型)”,属性值指向“自己所属类的原型prototype”

  • 对象数据类型值
    • 普通对象
    • 特殊对象:数组、正则、日期、Math、Error…
    • 函数对象
    • 实例对象
    • 构造函数.prototype
    • ....

案例:

function Fn() {
      this.x = 100;
   this.y = 200;
      this.getX = function() {
       console.log(this.x);
      }
}
Fn.prototype.getX = function() {
   console.log(this.x);
  };
Fn.prototype.getY = function() {
    console.log(this.y);
};
let f1 = new Fn;
let f2 = new Fn;
  console.log(f1.getX === f2.getX);
console.log(f1.getY === f2.getY);
console.log(f1.__proto__.getY === Fn.prototype.getY);
console.log(f1.__proto__.getX === f2.getX);
console.log(f1.getX === Fn.prototype.getX);
console.log(f1.constructor);
  console.log(Fn.prototype.__proto__.constructor);
f1.getX();
f1.__proto__.getX();
f2.getY();
Fn.prototype.getY();

Snipaste_2021-08-10_23-46-24

微信图片_20211204001423

function C1(name) {
   if (name) {//name = undefined -> false
        this.name = name;
   }
}
function C2(name) {
    this.name = name;//name = undefined
}
function C3(name) {
   this.name = name || 'join';//name = undefined->name = join
}
C1.prototype.name = 'Tom';
C2.prototype.name = 'Tom';
C3.prototype.name = 'Tom';
alert((new C1().name) + (new C2().name) + (new C3().name));//Tom+undefined+join

3、基于内置类原型扩展方法

内置类原型指向图:arr -> Array.prototype -> Object.prototype

Array

微信图片_20210811011405


/*
yuanlia在内置类原型上自定义(或扩展)属性方法,供其实例调取使用
  [优势]
  + 操作起来更贴近于“原生”的操作  例如:arr.unique() 而不是之前的 unique(arr)
  + 方法中的this一般都是当前类的实例,直接去操作即可(不需要额外判断数据的类型)
    + 可以实现链式写法:方法执行完返回的结果,“依然是当前类的实例”,这样就可以继续调用其它的方法进行操作了
  + ...
    [特殊]
  + 我们自己扩展的方法,最好设置前缀,例如:myXxx,这样可以防止自己写的方法覆盖了内置的属性方法,导致不可控的BUG
*/
//创建unique方法并调用
const unique = function unique(arr) {
   // 首先应该校验传递arr值,类型的合法性
   return Array.from(new Set(arr));
};
let arr = [1, 3, 2, 3, 2, 1, 2, 3, 4, 5, 2, 1, 2, 3, 4, 5];
arr = unique(arr);
arr.sort((a, b) => a - b);
console.log(arr);

//基于内置类原型扩展方法「实例可以直接调用」:设置的名字不要和内置的方法名冲突{一般自己会设置前缀 myXxx}
Array.prototype.myUnique = function myUnique() {
   // this -> 我们要处理的数组,this指向arr,arr为当前的数组
   return Array.from(new Set(this));
};
let arr = [1, 3, 2, 3, 2, 1, 2, 3, 4, 5, 2, 1, 2, 3, 4, 5];
arr = arr.myUnique().sort((a, b) => a - b);
console.log(arr);

// 调用所属类原型上的内置属性方法 实例.方法()
//  + 方便
//  + 实现链式写法「执行完一个方法,返回的结果还是当前类的实例,这样就可以继续调用,类为其提供的其它方法执行」
arr.sort((a, b) => a - b).map(item => item * 10).filter(item => item < 50).push('zhufeng');

---------------------------------------------------------------------------------------------

/* 
原始值类型的特殊性
 10 是个数字,它本身是不具备键值对的,但是它是Number类的实例,可以调用Number.prototype上的方法
 (10).toFixed(2) 装箱的操作:浏览器首先会把原始值变为对象类型的实例「new Number(10) 或者 Object(10)」,然后再继续调用toFixed等方法...
 new Number(10) 是Number类的实例,它的结果是对象类型的,所以可以调用Number.prototype上的方法,但是它可以进行数学运算
 let n=new Number(10);
 n+10 拆箱的操作:把对象类型的实例变为原始值类型的值{Number(对象)} Symbol.toPrimitive -> valueOf -> toString -> 变为数字 
*/
(function(proto) {
   // 检测num是否是合法数字
   const checkNum = num => {
       num = +num;
       return isNaN(num) ? 0 : num;
   };

   proto.plus = function plus(num) {
       num = checkNum(num);
         return this + num; //this指向是n,n=10
   };
 
   proto.minus = function minus(num) {
       num = checkNum(num);
       return this - num;
   };
})(Number.prototype);
 
let n = 10;
let m = n.plus(10).minus(5);
console.log(m); //=>15(10+10-5)

4、私有和公有属性的检测

Object.prototype.hasOwnProperty:用来检测是否为私有属性

  • 语法:[对象].hasOwnProperty([属性])
  • 检测[属性]是否为[对象]的私有属性,是返回TRUE,不是则返回FALSE;只看私有中有没有(和公有是否存在没关系);

in操作符

  • 语法:[属性] in [对象]
  • 检测[属性]是否率属于这个[对象],不论公有还是私有,只要能访问到这个属性,则结果就是TRUE
// 私有属性还是公有属性本身就是相对的概念:自己堆内存中的是私有属性,基于__proto__查找的是“相对自己”公有属性
let arr = [10, 20];
// arr.push(30); //arr首先找自己私有的属性,发现没有push方法,则默认基于__proto__去Array.prototype上找;所以它是找到Array.prototype.push方法,把其执行「this->arr 实参->30」;而push方法执行的作用,就是给arr(this)的末尾追加新的内容30,并且让数组长度累加1,返回新增后数组长度!!
console.log(arr.hasOwnProperty('push')); //false
console.log(Array.prototype.hasOwnProperty('push')); //true
console.log('push' in arr); //true 
  • Object.prototype.hasOwnProperty

微信图片_20210811011425

  • in操作符

微信图片_20210811011431

  • 面试题:在Object.prototype.hasPubProperty,用户来检测当前属性是否为对象的公有属性(无关私有中是否存在)
/* 检测当前属性是否为对象的 私有/公有属性 */

// 思路一:是它的属性,但是还不是私有的,这样只能是公有的属性(不推荐, 由bug)
// BUG:如果这个属性即是私有的也是公有的,这种方式检测就不准确了
Object.prototype.hasPubProperty = function hasPubProperty(attr) {
    // this -> 要检测的对象 attr -> 检测的属性
    return (attr in this) && !this.hasOwnProperty(attr);
};

// 思路二:跳过私有的查找「无论私有有没有,都无所谓」,直接找公共的「所属类的原型 先找自己所属类的原型,没有基于__proto__再继续向上找,直到Object.prototype为止;如果中间找到返回true,找到头都没有就是false!」
Object.prototype.hasPubProperty = function hasPubProperty(attr) {
    // this->obj要处理的对象  attr->'toString'要检测的属性
    // 思路:跳过私有属性的查找,直接在公有属性中查找,看看是否存在
    // Object.getPrototypeOf([实例对象]):获取当前实例对象的原型对象(或者获取“实例对象.__proto__”)
    let proto = Object.getPrototypeOf(this);
    while (proto) {
        if (proto.hasOwnProperty(attr)) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
};

let arr = [10, 20, 30];
arr.push = 'push';
console.log(arr.hasPubProperty('0')); //false
console.log(arr.hasPubProperty('push')); //true
console.log(arr.hasPubProperty('AAA')); //false

5、CLASS创造类的语法

// 基于Class语法,设置的原型上的公共方法(或者静态私有方法)都是不可枚举的属性;但是基于 xxx=xxx 这种写法,设置的属性是可枚举的;
// 基于Class语法创造的构造函数,是不能当做普通函数执行的 Uncaught TypeError: Class constructor Modal cannot be invoked without 'new'
class Modal {
    /* 构造函数体
    constructor方法在这里实质就是构造方法,通过new生成实例会自动调用,如果类型没有定义constructor则会默认添加
    */
    constructor(x, y) {
            this.x = x;
            this.y = y;
        };
    // 给当前实例设置私有的属性和方法
    z = 10;
    getX = () => {};

    /* 
     * 向原型上扩展方法 
     *   + 不能扩展公共属性、增加的方法不能是箭头函数「因为语法只能按照下述方式写」
     *   + 增加的方法没有prototype属性
     *   + 这些方法是不可枚举的
     * 我们可以在外面,基于Modal.prototype.xxx去弥补上述出现的情况
     */
    getX() {
        console.log(this.x);
    }
    getY() {
        console.log(this.y);
    }

    /* 设置静态私有属性方法 */
    // 定义在构造函数上的属性。只能通过构造函数名来调用。
    // 使用静态方法时,无需实例化对象,便可以调用,对象实例不能调用对象的静态方法,只能调用实例自身的静态属性和方法。
    static n = 200;
    static setNumber() {
        this.n = n;
    }
}
// Modal(); //Uncaught TypeError: Class constructor Modal cannot be invoked without 'new'  基于class创建的构造函数,只能带new执行,也就是只能基于构造函数的方式去执行,不能当做普通函数执行「比ES5严谨很多」
console.log(Modal.n); //200 静态私有属性方法通过类直接调用,
let m = new Modal(10, 20);
console.dir(m);
console.dir(Modal);

Snipaste_2021-12-11_12-51-01

function Modal(x, y) {
    /!* 构造函数体:给实例设置私有属性方法 *!/
    this.x = x;
    this.y = y;
}
/!* 在构造函数的原型上设置实例可调用的公共属性方法  实例.xxx *!/
Modal.prototype.z = 10;
Modal.prototype.getX = function () {
    console.log(this.x);
};
Modal.prototype.getY = function () {
    console.log(this.y);
};
/!* 把其当做普通对象,设置的静态私有属性方法  Modal.xxx  *!/
Modal.n = 200;
Modal.setNumber = function (n) {
    this.n = n;
};

let m = new Modal(10, 20);
console.dir(m);
console.dir(Modal);

Snipaste_2022-01-06_15-57-12


6、类继承的多种常用方案和原理

面向对象中:类的“继承、封装、多态”

  • 封装:把实现某个功能的代码进行封装处理,后期想实现这个功能,直接调用函数执行即可“低耦合、高内聚”

  • 多态:重载「方法名相同,参数类型或者个数不同,这样会认为是多个方法」& 重写「子类重写父类中的方法」

  • 继承:子类继承父类的方法“子类的实例即拥有子类赋予的私有/公有属性方法,也想拥有父类赋予的私有/公有属性方法”

@1 原型继承

  • 特点:和传统后台语言中的继承不一样「后台:子类继承父类,会把父类的方法COPY一份给子类」,并没有把父类的方法copy一份给子类,而是建立子类和父类之间的原型链指向,后期子类实例访问父类中提供的属性方法,也是基于__proto__原型链一层层查找的
    • 父类想要赋予其实例私有的属性x,此时变为了子类实例ch的公有属性
    • 子类实例可以基于原型链,修改父类原型上的方法「这样会对父类的其他实例也产生影响」
  • function Parent() {
        this.x = 100;
    }
    Parent.prototype.getX = function () {};
                                                                                            
    function Child() {
        this.y = 200;
    }
    Child.prototype = new Parent;
    Child.prototype.getY = function () {};
                                                                                            
    let ch = new Child;
    console.dir(ch);
    

**@2 call继承 :**把父类当做普通方法执行「原型啥的就没啥作用了」,让方法中的THIS是子类的实例,这样可以达到让父类中赋予其实例私有的属性,最后也变为子类实例私有的属性

  • 有问题:父类想要赋予其实例私有的属性x,不仅在子类实例的私有属性中存在,也在子类实例的公有属性中存在
function Parent() {
    this.x = 100;
}
Parent.prototype.getX = function () {};

function Child() {
    Parent.call(this);//ch.x=100
    this.y = 200;
}
Child.prototype = new Parent;
Child.prototype.getY = function () {};

let ch = new Child;
console.dir(ch);

@3寄生组合式继承 把call继承和变形的原型继承混合在一起,就实现了“寄生组合式继承”「推荐」

  • Child.prototype = Object.create(Parent.prototype);

  • function Parent() {
        this.x = 100;
    }
    Parent.prototype.getX = function () {};
                                                                                            
    function Child() {
        Parent.call(this);
        this.y = 200;
    }
    Child.prototype = Object.create(Parent.prototype);//创建一个空实例对象
    Child.prototype.getY = function () {};
                                                                                            
    let ch = new Child;
    console.dir(ch);
    

xcx

@4 ES6中类的继承 extends「非常类似于寄生组合继承」

class Parent {
    constructor() {
        this.x = 100;
    }
    getX() {}
}

class Child extends Parent {
    /* constructor() {
        // 一但使用extends,并且编写了constructor,必须在constructor函数第一行写上 super()
        //   从原理上类似call继承  super() ==> Parent.call(this)
        super();
        this.y = 200;
    } */
    y = 200;
    getY() {}
}

let ch = new Child;
console.dir(ch);

7、进阶:函数的三种角色

function Foo() {
    getName = function () {
        console.log(1);
    };
    return this;
}
Foo.getName = function () {
    console.log(2);
};
Foo.prototype.getName = function () {
    console.log(3);
};
var getName = function () {
    console.log(4);
};
function getName() {
    console.log(5);
}
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

微信图片_20211206002805

1

8、函数在__proto__和prototype之间的关系

Snipaste_2021-08-12_02-08-32

微信图片_20211206002755


9、进阶:重写内置NEW

微信图片_20210812235523

面试题:假如没有new关键词,需要我们自己编写_new方法,实现出和new相同的效果

function Dog(name) {
    this.name = name;
}

Dog.prototype.bark = function() {
    console.log('wangwang');
}
Dog.prototype.sayName = function() {
        console.log('my name is ' + this.name);
    }
    /*
    let sanmao = new Dog('三毛'); 
    sanmao.sayName();
    sanmao.bark();
    */
function _new() {
    //=>完成你的代码   
}
let sanmao = _new(Dog, '三毛');
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true

答案:

function Dog(name) {
    this.name = name;
}
Dog.prototype.bark = function() {
    console.log('wangwang');
}
Dog.prototype.sayName = function() {
    console.log('my name is ' + this.name);
}

/* // Ctor:constructor缩写 想创在哪一个构造函数的实例
// params:实参集合 未来传递给构造函数的实参
function _new(Ctor, ...params) {
    // @1 创造当前类的一个实例{空实例对象}
    //  实例对象.__proto__ 指向 构造函数.prototype
    let obj = {};
    obj.__proto__ = Ctor.prototype;
    // @2 像普通函数执行一样把构造函数执行,让函数中的THIS指向创建的实例对象
    let result = Ctor.call(obj, ...params);
    // @3 看函数的返回值,如果返回的是个对象,则以自己返回的为主,否则把创建的实例对象返回
    if (result !== null && (typeof result === "object" || typeof result === "function")) return result;
    return obj;
}
*/
/* function _new(Ctor, ...params) {
    /!* let obj = {},
        result;
    Object.setPrototypeOf(obj, Ctor.prototype); //给某个对象设置原型指向(也就是让obj.__proto__===Ctor.prototype),只兼容IE11及以上 *!/
    // Object.create(proto):创建一个空对象,并且把proto作为空对象的原型指向(空对象.__proto__===proto);proto必须是对象或者null;如果是null则是创造一个没有原型指向的对象; 不兼容IE6~8
    let obj = Object.create(Ctor.prototype),
        result;
    result = Ctor.call(obj, ...params);
    if (result !== null && /^(object|function)$/.test(typeof result)) return result;
    return obj;
} */
/* function _new(Ctor, ...params) {
    let obj,
        result,
        proto = Ctor.prototype;
    if (!proto || Ctor === Symbol || Ctor === BigInt) throw new TypeError('Ctor is not a constructor');
    obj = Object.create(proto);
    result = Ctor.call(obj, ...params);
    if (result !== null && /^(object|function)$/.test(typeof result)) return result;
    return obj;
} */

/* 重写内置NEW */
const _new = function _new(Ctor, ...params) {
    // 对Ctor的格式做校验:必须是个函数 & 必须具备prototype属性 & 不能是生成器函数 & 排除Symbol/BigInt ...
    if (Ctor === Symbol || Ctor === BigInt || Object.prototype.toString.call(Ctor) !== "[object Function]" || !Ctor.prototype) throw new TypeError(`Ctor is not a constructor`);
    // 核心处理
    let obj,
        result;
    obj = Object.create(Ctor.prototype);
    result = Ctor.call(obj, ...params);
    if (result !== null && /^(object|function)$/.test(typeof result)) return result;
    return obj;
};

const _new = function _new(Ctor) {
   // 对Ctor的格式做校验:必须是个函数 & 必须具备prototype属性 & 不能是生成器函数 & 排除Symbol/BigInt ...
    if (Ctor === Symbol || Ctor === BigInt || Object.prototype.toString.call(Ctor) !== "[object Function]" || !Ctor.prototype) throw new TypeError(`Ctor is not a constructor`);
    var obj, result, params;
    params = [].slice.call(arguments, 1);
    obj = Object.create(Ctor.prototype);
    result = Ctor.apply(obj, params);
    if (result !== null && /^(object|function)$/.test(typeof result)) return result;
    return obj;
}

let sanmao = _new(Dog, '三毛');
sanmao.bark(); //=>"wangwang"
sanmao.sayName(); //=>"my name is 三毛"
console.log(sanmao instanceof Dog); //=>true

Object.setPrototypeOf(obj, Ctor.prototype); //给某个对象设置原型指向(也就是让obj.proto===Ctor.prototype),只兼容IE11及以上

模拟slice实现克隆

Array.prototype.slice = function slice() {
    // this->arr
    var arg = [];
    for (var i = 0; i < this.length; i++) {
        arg.push(this[i]);
    }
    return arg;
};
// let arr = [10, 20, 30, 40];
// let arr2 = arr.slice();
// console.log(arr2);

function fn() {
    // console.log(arguments); //类数组集合,不能直接使用Array.prototype上的方法
    // console.log(arguments.slice(1)); //Uncaught TypeError: arguments.slice is not a function

    // 我们如果可以把arguments变为数组集合即可?
    //var arg = Array.from(arguments); // ES6新增的, IE都不兼容
    //console.log(arg);
    var arg = [];
    for (var i = 0; i < arguments.length; i++) {
        arg.push(arguments[i]);
    }
    console.log(arg);
    // 通过对比,我们发现:只要我们把Array.prototype.slice方法执行,让方法中的this指向arguments,就相当于循环arguments中的每一项,并且赋值给新数组,实现把类数组转换为数组「前提:arguments类数组集合和数组结构基本一致,操作的代码也基本一致,只不过是不能直接用Array.prototype上的这些方法而已」
    //  + 把slice执行  Array.prototype.slice() 或者 [].slice()
    //  + 改变this  call方法
    var arg = [].slice.call(arguments);
    console.log(arg);
    // 类数组借用数据原型上的方法进行操作:大部分方法都可以基于这种方式借用  [].xxx.call([类数组],[实参]...)
}
fn(10, 20, 30, 40, 50);

专题:JS中THIS的五种情况梳理

This:函数的执行主体【谁把它执行的,和函数在哪执行以及在哪定义都没有直接的关系】

  • 全局上下文中的THIS事window/undefined
  • 块级上下文没有自己的THIS,所有用到的THIS都是继承其上级上下文中的【宿主环境】
  • 所以平时我们平时研究的都是函数中的THIS指向

@1 DOM元素事件绑定:给当前元素的某个事件行为绑定方法,当事件行为触发,方法执行,方法中的THIS是当前DOM元素

document.body.onclick = function() {
    // 点击执行:this -> body
};
document.body.addEventListener('click', function() {
    // 点击执行:this -> body
});

@2 普通函数执行,看方法名前面是否有“点”

  • 有“点”,那么它前面是谁THIS就是谁;
  • 没有“点”,THIS就是window{非严格模式}/undefined{严格模式}
const fn = function fn() { console.log(this); };
let obj = {
    name: 'zhufeng',
    fn
};
fn(); //this->window
obj.fn(); //this->obj

arr.push(100):arr基于_proto_,找到Array.prototype.push方法,把push方法执行,实现数组新增;此时push中的this->arr;

arr._proto_.push(100):直接找到Array.prototype.push这个方法并且执行,此时push中的this->arr.proto;

==>内置push方法实现了“方法中的THIS是谁,就把100加入到谁的末尾”

一般情况下,匿名函数【自执行函数&回调函数】执行,方法中的THIS一般都是window/undefined,除了做过特殊的处理;

(function() {
    //this -> window/undefined
})();

arr.forEach(function() {
    //this -> window/undefined
});
//迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
//forEach等数组迭代方法,传递第二个参数的目的:修改数组每一次迭代执行的回调函数中的THIS指向【forEach内部特殊的处理】
arr.forEach(function() {
    //this -> obj
}, obj);

@3 构造函数执行【NEW执行】,构造函数体中的THIS指向当前类的实例

class Demo {
    constructor() {
        //this->d1
    }
}
let d1 = new Demo;

@4 箭头函数中没有自己的THIS【类似于块级上下文】,在函数中用到的THIS是所处上下文中的;

let obj = {
    name: 'zhufeng',
    fn() {
        // this->obj
        let self = this;

        setTimeout(function() {
            console.log(this); //this->window
            self.name = '珠峰培训';
        }, 1000);

        setTimeout(() => {
            this.name = "第二次处理"; //obj.nme='...'
        }, 1000);
    }
};
obj.fn();

@5 我们可以基于call/apply/bind三个方法,简单粗暴的改写函数中的THIS指向

Function.prototype

  • call
  • apply
  • bind

所有的函数都是Function类的实例,所以所有函数都可以调用这三个方法;而这个三个方法都是用来改变函数中的THIS指向的;

fn.call(obj,10,20)

把fn执行,传递10/20两个实参,让函数中的THIS指向obj

详细描述:

首先看fn看自己的静态私有属性方法中是否有call等方法,如果有则执行这个方法;如果没有,则默认基于__proto__去找Function.prototype上的call方法;把call方法执行的时候:

  • call中的this -> fn 后期函数中要修改的this -> obj 传递10&20
  • call方法内部,会把fn[call中的this]执行,让函数中的this指向obj,传递10&20实参
fn.apply(obj,[10,20])

和call只有一个区别【从第二个参数开始,会把所有需要传递给函数的实参,以数组的方式进行处理】

------在三个及以上参数的情况下,call的性能要大于apply的性能

fn.bind(obj,[10,20])

const fn = function(n,m){};

document.body.onclick =fn;//this -> body n -> ev m -> undefined //ev(事件的对象)

基于call/apply改变THIS的时候,会同时需要执行的函数把函数立即执行【立即执行函数、立即改变THIS】;而bind不会立即改变this,也不会立即把函数执行,而是预先把传递的obj、10、20以及最后要执行的方法fn存储起来

document.body.onclick=fn.bind(obj,10,20)

call VS apply
  • 都是把函数立即执行,改变函数中的this指向的「第一个参数是谁,就把this改为谁」
  • 唯一区别:apply要求把传递给函数的实参,以数组的形式管理起来「最终效果和call一样,也是把数组中每一项,一个个的传给函数」
  • 真实项目中建议大家使用call,因为其性能好一些「做过测试:三个及以上参数,call的性能明显比apply好一些」
call VS bind
  • call是把函数立即执行,而bind只是预处理函数中的this和参数,函数此时并没有执行

vue 或者 react中也好,尤其是vue中,本身就是个框架,内部对this做了一些处理,保障响应式数据和方法方法中的this都是当前类的实例

Function.prototype.bind = function bind(context, ...params) {
    // this->fn  context->obj  params->[10,20]
    let self = this;
    return function proxy(...args) {
        // args->[ev] this->submit
        params = params.concat(args);
        return self.call(context, ...params);
    };
};

const submit = document.querySelector('#submit');
const obj = { name: 'obj' };
const fn = function fn(x, y, ev) {
    console.log(this, x, y, ev);
};
// submit.onclick = fn; //点击按钮 this->submit  x->PointerEvent 
// 需求:点击按钮,fn方法执行,我们想让其中的this变为obj,ev事件对象也存在,再传递10/20
// submit.onclick = fn.call(obj, 10, 20); //这样处理是错误的,因为call是把函数立即执行,还没点击呢,fn就执行了...
/* submit.onclick = function (ev) {
    // 先给事件行为绑定匿名函数,当点击的时候,先执行匿名函数「获取到ev事件对象了」;在匿名函数执行的时候(this->submit),我们再让真正要处理的fn函数执行,此时就可以基于call去改变this了!!
    fn.call(obj, 10, 20, ev);
}; */
submit.onclick = fn.bind(obj, 10, 20); //这样处理就可以了
// submit.onclick = proxy;

案例

Function.prototype.call = function call(context, ...params) {
    //this -> fn    context -> obj   params -> [10,20]
    //把fn执行,让fn中的this指向obj,并且把10/20传递给fn,接受其返回值然后再返回
    //如果context是原始值类型的值,则需要把其变为对象类型【原因:原始值设置属性,属性是无效的】{n=1;n.xxx=1;n.xxx报错}
    if (context == null) context = window;
    if (!/^(object|function)$/.test(typeof context)) context = Object(context); //如果传的不是对象类型,使用Object()方法将其变为对象类型
    let key = Symbol('KEY'), //新增的属性防止和context原始属性冲突
        result;
    context[key] = this; //让两者关联
    result = context[key](...params); //把方法执行, 而且方法中的this就是context
    //delete context[key]; //把新增的属性移除掉
    Reflect.deleteProperty(context, key); //ES6中,可以基于Reflect.deleteProperty移除对象的属性
    return result; //把函数执行的返回值返回
}
const fn = function fn(x, y) {
    console.log(this, x, y);
    return x + y;
};
let obj = {
    name: 'zhufeng',
    xxx: 100
};
// fn(); //this->window
// obj.fn(); //Uncaught TypeError: obj.fn is not a function
let res = fn.call(obj, 10, 20);
console.log(res);
Function.prototype.call = function call(context, ...params) {
    if (context == null) context = window;
    if (!/^(object|function)$/.test(typeof context)) context = Object(context);
    let self = this,
        key = Symbol('KEY'),
        result;
    context[key] = self;
    result = context[key](...params);
    Reflect.deleteProperty(context, key);
    return result;
};
const fn = function fn(x, y) {
    console.log(this, x, y);
    return x + y;
};
let obj = {
    name: 'obj'
};
let res = fn.call('zhufeng', 10, 20);
console.log(res);
//@1 fn基于__proto__找到Function.prototype.call,把call方法执行;
//@2 在call方法执行的时候:
//  + context:obj 要改变的THIS指向  params:[10,20] 执行函数传递的实参信息  this:fn 要执行的函数
//  + 它干的事情是:立即把fn(this)执行,并且让fn(this)中的this指向obj(context),把10/20(params)传递给fn(this),接收fn(this)执行的返回结果,作为最后的结果返回 
const fn = function fn(x, y) {
    console.log(this, x, y);
};
let obj = {
    name: 'obj'
};
// obj.fn(10, 20); //Uncaught TypeError: obj.fn is not a function
// fn.call(obj, 10, 20); //this->obj x->10 y->20
// fn.apply(obj, [10, 20]); //this->obj x->10 y->20
// fn.call(10, 20); //this->new Number(10) x->20 y->undefined  「this存储的值:null/undefined/对象」
// fn.call(); //this->window/undefined(严格模式)
// fn.call(null); //this->window/null(严格模式) 

// 获取数组最大值
let arr = [12, 23, 13, 24, 32, 15];
// console.log(Math.max(arr)); //NaN
// console.log(Math.max(12, 23, 13, 24, 32, 15)); //32  Math.max要求一项项的传递数字,不能直接传递数组
// console.log(Math.max(...arr)); //32  ES6中的展开运算符
// console.log(Math.max.apply(Math, arr)); //32 

let obj = {
    // fn:fn
    fn,
    name: 'obj'
};
// fn(10, 20); //this->window x->10 y->20
// obj.fn(10, 20); //this->obj x->10 y->20 *

专题:JS中检测数据类型的方法

typeof[value]:检测数据类型的运算符

@1 返回结果是一个字符串,其次字符串中包含对应的数据类型,例如:"number","object","function"....

typeof typeof typeof[1,2,3] => "string"

@2 弊端
  • typeof null -> "object"
  • typeof检测对象,除函数对象会返回“function”,其余对象返回的都是“object”「不能细分对象」
  • typeof 未被声明的变量 -> 不会报错,而是"undefined"

除了这些以外,用typeof监视原始值类型【或者函数类型】还是非常方便、准确的

@3 检测的原理

所有数据类型在计算机底层都是按照二进制的值来进行存储的,而typeof就是按照二进制的值进行检测的

  • 性能好
  • 对象的二进制值开头都是“000”,而null的二进制值都是零,所以typeof检测null的时候,识别其实一个对象(这样是不对的);如果识别为对象,再看对象是否实现了call方法,实现了call方法的返回“function”,没有实现的统一返回“object”;

@4 应用

  • 检测除null之外的原始值类型可以使用他
  • 检测是否为对象 if(obj!==null && /^(object|function)$/.test(typeof obj)){...}
  • 检测某个东西是否兼容 if(typeof Symbol!=="undefined"){...}
  • ...

instanceof:检测当前实例是否属于这个类【临时拉来做数据类型检测】

@1 [对象] instanceof [构造函数] 检测对象是否为这个类的实例,基于这个特点可以“临时”拿来检测数据类型,返回true/false
@2 可以做一些数据类型的检测【对typeof做了一个补充,可以适当的细分一下对象】
@3 弊端:
  • 无法基于instanceof检测是否为标准普通对象【纯粹对象:直接是Object类的实例】;因为所有对象都是Object类的实例,基于“xxx instanceof Object”检测的时候,返回的结果都是true;

  • 对原始值类型无效:instanceof左侧只要是原始值类型,结果就是false,默认不会进行“装箱”

    例如:10 instanceof Number -> false

  • 检测的结果不一定严谨;因为可以修改原型链的指向

  • .....

@4 当我们基于“【value】instanceof [Ctor]”运算符进行数据类型的检测的时候
  • 传统版本:如果不存在这个函数,浏览器会按照当前【value】原型链一层一层向上找,直到找到Object.prototype为止;查看[Ctor].prototype是否出现在它的原型链中,如果出现了,则结果是true,说明[value]是[Ctor]的实例,反之则为false...

  • 新版本:首先调用【Ctor】【Symbol.hasInstance】(value)这个函数,如果存在这个函数,则直接基于这个函数处理即可当代浏览器基本都有,因为Symbol.hasInstance在Function.prototype中,每一个构造函数都是Function的实例,都可以调用Function.prototype[Symbol.hasInstance]这个方法,当我们基于 “[对象] instanceof [构造函数]”检测处理的时候,内部是这样处理的:

    ​ [构造函数][Symbol.hasInstance]([对象])

相关代码
let obj = {};
let arr = [];
let reg = /^$/;
let num = new Number(10);

console.log(arr instanceof Array); //true 
console.log(obj instanceof Array); //false
console.log(reg instanceof Array); //false
console.log(typeof num); //"object"
console.log(num instanceof Number); //true 说明num是Number类的一个实例「原始值对应的对象类型结果」

console.log(arr instanceof Array); //true 
console.log(arr instanceof Object); //true 
console.log(reg instanceof Object); //true 
console.log(obj instanceof Object); //true 
console.log(num instanceof Object); //true

//可以修改原型链的指向
const Fn = function Fn() {};
Fn.prototype = Array.prototype;
let f = new Fn;
console.log(f); //从结构来看,f一定不是数组「数组的结构:数字索引、逐级递增、length属性...」
console.log(f instanceof Array); //true

const Fn = function Fn() {
 this.name = 'zhufeng';
};
Fn.prototype.sayName = function() {};
Fn.xxx = 'xxx';
Fn[Symbol.hasInstance] = function() { //这样设置是无效的
 console.log(1);
 return false;
};
Function.prototype[Symbol.hasInstance] = function() { //这样设置也是无效的
 console.log(1);
 return false;
};
let f = new Fn;
console.log(f instanceof Fn);

//------------------------------------

class Fn {
 name = 'zhufeng';
 sayName() {}
     // 当做对象,设置静态私有的属性方法「这样设置是有用的,所以重构“构造函数”的Symbol.hasInstance,只支持ES6中class创建的类,ES5中创建的构造函数不支持这样重构」
 static xxx = 'xxx';
 static[Symbol.hasInstance](obj) {
     console.log(obj);
     return false;
 }
}
let f = new Fn;
console.log(f instanceof Fn); //false
class Fn {
 name = 'Fn';
 x = 10;
 y = 20;
 sum() {
         if (!this.name) throw new TypeError('this.name is not defined');
         // ...
     }
     // 只要基于ES6中的class创建类,基于“static xxx”这种语法重写Symbol.hasInstance才有用;如果是ES5创建的类,基于 Fn[Symbol.hasInstance]=xxx 这样重写是无效的!!
 static [Symbol.hasInstance](obj) {
     return (obj.name && Object.getPrototypeOf(obj) === Fn.prototype) ? true : false;
 }
}
let f1 = new Fn;
let f2 = new Fn;
console.log(f1 instanceof Fn); //true
f1.sum();

let obj = {};
Object.setPrototypeOf(obj, Fn.prototype);
console.log(obj instanceof Fn); //Fn[Symbol.hasInstance](obj) false
// console.log(obj instanceof Fn); //true
// obj.sum(); //报错

@5 分析instanceof的优缺点和底层实现机制,并且重写instancof
/* 
  instance_of:检测value是否为Ctor的实例
  value:要检测的实例
  Ctor:要检测的构造函数
 */
const instance_of = function instance_of(value, Ctor) {
    // 保证Ctor的格式也是正确的 要是一个函数,并且有prototype属性
    if (typeof Ctor !== "function") throw new TypeError('Right-hand side of instanceof is not callable');
    if (!Ctor.prototype) throw new TypeError('Function has non-object prototype in instanceof check');

    // 不支持原始值类型值的检测
    if (value === null) return false;
    if (!/^(object|function)$/.test(typeof value)) return false;

    // 支持Symbol.hasInstance方法的直接按照这个处理
    //if (typeof Ctor[Symbol.hasInstance] === "function") return Ctor[Symbol.hasInstance](value);
	if (typeof Symbol !== "undefined") return Ctor[Symbol.hasInstance](obj);
    
    // 不支持的则按照原型链一层层的查找即可 Object.getPrototypeOf(value):获取value所属构造函数的原型对象
    let proto = Object.getPrototypeOf(value);
    while (proto) {
        // Ctor.prototype出现在了value的原型链上「value是Ctor的实例对象」:直接返回true & 结束查找
        if (proto === Ctor.prototype) return true;
        proto = Object.getPrototypeOf(proto);
    }
    return false;
};

Object.getPrototypeOf(value):获取value所属构造函数的原型对象


constructor:获取当前实例所属的构造函数【临时】

@1 [value].contructor 获取其构造函数,验证是否为我们想检测的类

​ 例如:[value].constructor===Array

@2 相比较于instanceof来讲
  • 可以检测原始值类型的值【排除null/undefined】
  • 检测是否为纯粹对象(标准普通对象) [value].constructor===Object
  • 和instanceof一样,检测的结果仅供参考【constructor这个值是可以被肆意修改的】
相关代码
//--------------constructor
let arr = [];
let reg = /^$/;
let num = 10;

console.log(arr.constructor === Array); //true
console.log(arr.constructor === Object); //false
console.log(reg.constructor === Array); //false
console.log(num.constructor === Number); //true

Object.prototype.toString.call([value]):专门检测数据类型的方法

@1 调用Object.prototype.toString方法,让方法中的this指向检测的值,就是检测当前值的数据类型

​ 返回的结果是一个字符串”[object ?]“

​ 例如:Object.prototype.toString.call(10) -> "[object Number]"

​ Object.prototype.toString.call(new Number(10)) -> "[object Number]"

​ 它是所有检测数据类型的办法中,最强大、最稳定...的方式{除了写起来麻烦一些}

@2 返回的结果是”[object ?]“,"?"会是啥呢?
  • 首先获取【value】[Symbol.toStringTag]属性值。如果存在这个属性,则这个属性值是什么,”?“就是什么

  • 如果没有这个属性,一般“?”是当前实例所属的构造函数

    Symbol.prototype & BigInt.prototype & Math & GeneratorFunction.prototype & Promise.prototype & Set.prototype & Map.prototype ... 这些类的原型上,都有Symbol.toStringTag这个属性

  • Object.prototype.toString这个方法是用来检测数据类型的,而且方法内部规定:方法中的this是谁,我们就检测谁的类型,所以我们基于call方法去改变this指向

  • 除Object.prototype.toString之外,其余构造函数原型上的toString一般都是用来“转换字符串的”,只有它是用来检测数据类型的,返回的结果 “[object ?]”

相关代码
//-------------Object.prototype.toString.call
let obj = {},
   toString = obj.toString; //->Object.prototype.toString 基本上所有的数据类型,所属构造函数的原型上都有toString方法,一般都是用来转换为字符串的,只有Object.prototype.toString是用来检测数据类型的
console.log(toString.call(10)); //"[object Number]"
console.log(toString.call(new Number(10))); //"[object Number]"
console.log(toString.call("zhufeng")); //"[object String]"
console.log(toString.call(true)); //"[object Boolean]"
console.log(toString.call(null)); //"[object Null]"
console.log(toString.call(undefined)); //"[object Undefined]"
console.log(toString.call(Symbol())); //"[object Symbol]"
console.log(toString.call(10 n)); //"[object BigInt]"
console.log(toString.call({})); //"[object Object]"
console.log(toString.call([])); //"[object Array]"
console.log(toString.call(/^$/)); //"[object RegExp]"
console.log(toString.call(function() {})); //"[object Function]"
console.log(toString.call(new Date())); //"[object Date]"
console.log(toString.call(new Error())); //"[object Error]"
console.log(toString.call(Math)); //"[object Math]"
console.log(toString.call(function*() {})); //"[object GeneratorFunction]"
console.log(toString.call(Promise.resolve())); //"[object Promise]"
// 即使constructor值被修改 或者 基于Object.setPrototypeOf重定向实例的原型指向,结果也是不变的!!所以 toString.call 这种办法检测的结果是非常可靠的!!
[object ?] 返回自定义值
//自定义的构造函数一般返回的结果都是Object
// const Fn = function Fn() {};
// let f = new Fn;
// console.log(toString.call(f)); //“[object Object]”
// 需求:期望自己写自定义构造函数,所创建出来的实例在检测数据类型的时候,可以返回的是“[object 自己的构造函数]”

const Fn = function Fn() {};
Fn.prototype[Symbol.toStringTag] = "Fn";
let f = new Fn;
console.log(toString.call(f)); //“[object Fn]”

class Fn {
    [Symbol.toStringTag] = "Fn";
}
let f = new Fn;
console.log(Object.prototype.toString.call(f));//"[object Fn]"

编写工具类utils能实现类型检测

(function() {
 "use strict";
 /* 检测数据类型的 */
 const getProto = Object.getPrototypeOf,
     class2type = {},
     toString = class2type.toString, //Object.prototype.toString
     hasOwn = class2type.hasOwnProperty; //Object.prototype.hasOwnProperty
     //fnToString = hasOwn.toString,//Funtcion.prototype.toStirng
     //ObjectFunctionString = fnToString.call(Object);//'function Object(){native code}'
      

 // 检测是否为函数
 const isFunction = function isFunction(obj) {
     return typeof obj === "function" && typeof obj.nodeType !== "number" &&
         typeof obj.item !== "function";
 };

 // 检测是否为window对象
 const isWindow = function isWindow(obj) {
     return obj != null && obj === obj.window;
 };

 // 通用检测数据类型的办法,返回结果:字符串、含小写的数据类型
 const toType = function toType(obj) {
     let reg = /^\[object (.+)\]$/;
     if (obj == null) return obj + "";
     return typeof obj === "object" || typeof obj === "function" ?
         reg.exec(toString.call(obj))[1].toLowerCase() :
         typeof obj;
 };

 // 检测是否为标准普通对象(纯粹对象)
 const isPlainObject = function isPlainObject(obj) {
     let proto, Ctor;
     if (!obj || toString.call(obj) !== "[object Object]") return false;
     proto = getProto(obj);
     if (!proto) return true;
     //proto.hasOwnProperty('constructor') => hasOwn.call(proto, "constructor")
     Ctor = hasOwn.call(proto, "constructor") && proto.constructor;
     //fnToString.call(Ctor) === ObjectFunctionString
     //fnToString.call(Ctor) === fnToString.call(Object)
     return typeof Ctor === "function" && Ctor === Object;
 };

 // 检测是否为空对象
 const isEmptyObject = function isEmptyObject(obj) {
     if (obj == null || !/^(object|function)$/.test(typeof obj)) return false;
     let keys = Object.getOwnPropertyNames(obj);
     if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
     return keys.length === 0;
 };

 // 检测是否为数组或者类数组
 const isArrayLike = function isArrayLike(obj) {
     let length = !!obj && "length" in obj && obj.length,
         type = toType(obj);
     if (isFunction(obj) || isWindow(obj)) return false;
     return type === "array" || length === 0 ||
         typeof length === "number" && length > 0 && (length - 1) in obj;
 };


 /* 暴露API */
 const utils = {
     version: '1.0.0',
     isFunction,
     isWindow,
     toType,
     isPlainObject,
     isEmptyObject,
     isArrayLike
 };
 if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
 if (typeof window !== "undefined") window.utils = utils;
})();

Array.isArray:检测是否为数组

isNaN:检测是否为有效数字


面试题

alert({
    name: 'xxx'
}); //=>“[object Object]”  alert会把编写的值转换为字符串宰输出,而对象toString的时候,调用的是Object.prototype.toString这个方法,而这个方法是检测数据类型的
// 真正转换标准普通对象为字符串
//  + JSON.stringify 把对象变为 JSON 格式字符串  ->JSON.parse
//  + Qs.stringify 依托Qs第三方库,我们把对象变为 urlencoded 格式字符串
// 前后端数据通信的时候,我们经常需要把对象变为指定的字符串格式,传递给服务器;或者把服务器返回的指定格式字符串,变为对象!!

let obj = {
    name: 'zhufeng',
    age: 12,
    teacher: 'zhou'
};
// console.log(JSON.stringify(obj)); //'{"name":"zhufeng","age":12,"teacher":"zhou"}'
console.log(Qs.stringify(obj)); //'name=zhufeng&age=12&teacher=zhou'

练习

function fun() {
    this.a = 0;
    this.b = function () {
        alert(this.a);
    }
}
fun.prototype = {
    constructor:fun,
    b: function () {
        this.a = 20;
        alert(this.a);
    },
    c: function () {
        this.a = 30;
        alert(this.a)
    }
};
var my_fun = new fun();
my_fun.b();
my_fun.c();

微信图片_20211208233808

面试题

/*
 * 编写queryURLParams方法实现如下的效果(至少两种方案)
 */
let url="http://www.zhufengpeixun.cn/?lx=1&from=wx#video";
console.log(url.queryURLParams("from")); //=>"wx"
console.log(url.queryURLParams("_HASH")); //=>"video"
//@1
String.prototype.queryURLParams = function queryURLParams(attr) {
    let url = this,
        askIndex = url.indexOf('?'),
        weiIndex = url.lastIndexOf("#"),
        askText = '',
        weiText = '',
        params = {};

    /* 
        考虑问题:"#"是否存在;"?"是否存在;"#"和"?"谁在前面
        // ?和#都有,?在前:从“askIndex+1”~“wellIndex(不含)”、从“wellIndex+1”~末尾
        // ?和#都有,#在前:从“askIndex+1”~末尾、从“wellIndex+1”~“askIndex(不含)”
        // 只有?:从“askIndex+1”~末尾、“”
        // 只有#:“”、从“wellIndex+1”~末尾
    */
    if (askIndex == -1 && weiIndex >= 0) {
        weiText = url.substring(weiIndex + 1);
    } else if (askIndex >= 0 && weiIndex == -1) {
        askText = url.substring(askIndex + 1);
    } else if (askIndex >= 0 && weiIndex >= 0) {
        if (weiIndex > askIndex) {
            askText = url.substring(askIndex + 1, weiIndex);
            weiText = url.substring(weiIndex + 1);
        } else {
            weiText = url.substring(weiIndex + 1, askIndex);
            askText = url.substring(askIndex + 1);
        }
    }

    weiText !== "" ? params["_HASH"] = weiText : null;
    if (askText !== "") {
        let arr = askText.split("&");
        arr.forEach(item => {
            let [key, value] = item.split("=");
            params[key] = value;
        })
    }

    return typeof attr !== "undefined" ? params[attr] : params;
}


//@2
String.prototype.queryURLParams = function queryURLParams(attr) {
    let link = document.createElement("a"),
        url = this,
        askText = '',
        weiText = '',
        params = {};

    link.href = url;
    askText = link.search.substring(1);
    weiText = link.hash.substring(1);
    console.log(new URLSearchParams(askText))
    weiText !== "" ? params["_HASH"] = weiText : null;
    new URLSearchParams(askText).forEach((value, key) => { params[key] = value });
    return typeof attr !== "undefined" ? params[attr] : params;
}

//@3
String.prototype.queryURLParams = function queryURLParams(attr) {
     let self = this,
        params = {};
    // 从URL字符串中解析出“问号传参”&“哈希值”:赋值给obj对象的键值对
    // 方案二:基于正则处理
    self.replace(/#([^?#&=]+)/, (_, $1) => params['_HASH'] = $1);
    self.replace(/([^?=#&]+)=([^?=#&]+)/g, (_, $1, $2) => params[$1] = $2);

    // 控制结果返回
    return typeof attr !== "undefined" ? params[attr] : params;
}

微信图片_20211208212924

鸭子数据类型的几种常规处理

鸭子类型:所代表的是那些“类xxx”「例如:类数组、类Promise...」

类数组的结构和数组非常相似,所以导致了:操作数据的一些代码和方案对类数组也是适用的「例如:循环...」

但是因为类数组并不是Array的实例,所以不能直接调用Array.prototype上的方法

需求:让类数组也可以使用数组的方法「鸭子类型的含义」

  • 把类数组转换为数组

  • 修改原型链的指向:让类数组._proto_===Array.prototype => Object.setPrototypeOf([obj],[prototype])

  • 想借用数组的哪个办法,就把哪个办法设置为类数组的私有属性

  • 把借用的方法执行,让方法中的this指向要处理的类数组,这样也是可以的

A实例借用B实例拥有的方法:前提是A和B具备相类似的结果,操作B的代码和方案也是适用于A;后期只需要想办法让借用的方法执行,让方法中的THIS指向A,这样就相当于基于这个方法操作A...

let arr = [10, 20, 30];
let likeArr = {
    0: 10,
    1: 20,
    2: 30,
    length: 3,
    // forEach: Array.prototype.forEach //设置为类数组的私有属性
};

// Object.setPrototypeOf(likeArr, Array.prototype);
// likeArr.forEach((item, index) => {
//     console.log(item, index);
// }); 

// arr.forEach() //forEach中的this:arr   forEach内部的this是谁,它就把谁进行迭代循环
// likeArr.forEach((item, index) => {
//     console.log(item, index);
// });

// Array.prototype.forEach.call(likeArr, (item, index) => {
//     console.log(item, index);
// });
// [].forEach.call(likeArr, (item, index) => {
//     console.log(item, index);
// });
let elements = document.getElementsByTagName('*');
// 它是HTMLCollection集合/实例「类数组集合」,不能直接使用数组的办法,如果想使用数据的办法? 
// 前提条件:类数组结构和数组基本一致,操作数据的代码也适用于类数组
//   + 把其转换为数组
Array.from(elements).forEach(item => {
    console.log(item);
});
//   + 把数组的方法赋值给这个集合
elements.forEach = Array.prototype.forEach;
elements.forEach(item => {
    console.log(item);
});
//   + 基于call实现this的改变
// arr.forEach(item => {}); //arr基于__proto__找到Array.prototype.forEach,并且把它执行;forEach中的this是arr,此时就会迭代arr中的每一项...也就是forEach中的this是谁就会迭代谁
// Array.prototype.forEach...
[].forEach.call(elements, item => {
    console.log(item);
}); 
Array.prototype.push = function push(val) {
    // this->arr  val->10
    this[this.length] = val;
    this.length++;
    return this.length;
};
arr.push(10);

let obj = {
    2: 3, //1
    3: 4, //2
    length: 2, //4
    push: Array.prototype.push
};
obj.push(1);
// obj[obj.length]=1  obj.length++
obj.push(2);
// obj[obj.length]=2  obj.length++
console.log(obj);

转换为数组的方法

//转换为数组的方法
let utils = (function () {
    // function toArray(...params) {
    //     // ...剩余运算符获取的就是一个数组
    //     return params;
    // }

    function toArray() {
        // arguments 类数组集合
        //@1
        // return Array.from(arguments);
        //@2
        // return [...arguments];
        //@3
        return [].slice.call(arguments, 0);
    }
    return {
        toArray
    };
})();
let ary = utils.toArray(10, 20, 30); //=>[10,20,30]
console.log(ary);
ary = utils.toArray('A', 10, 20, 30); //=>['A',10,20,30]
console.log(ary);

输出下面代码的结果

var name = '珠峰培训';
function A(x,y){
    var res=x+y;
    console.log(res,this.name);
}
function B(x,y){
    var res=x-y;
    console.log(res,this.name);
}
B.call(A,40,30);
B.call.call.call(A,20,10);
Function.prototype.call(A,60,50);
Function.prototype.call.call.call(A,80,70);
Function.prototype.call = function call(context, ...params) {
    if (context == null) context = window;
    if (!/^(object|function)$/.test(typeof context)) context = Object(context);
    let self = this,
        key = Symbol('KEY'),
        result;
    context[key] = self;
    result = context[key](...params);
    Reflect.deleteProperty(context, key);
    return result;
};

var name = '珠峰培训';

function A(x, y) {
    var res = x + y;
    console.log(res, this.name);
}

function B(x, y) {
    var res = x - y;
    console.log(res, this.name);
}
B.call(A, 40, 30);
// 把call方法执行 this->B  context->A  params->[40,30]
// A[key]=B  A[key](40,30)  把B执行,让B中的this指向A,传递实参40/30  => 10 "A"

B.call.call.call(A, 20, 10);
// 把最后一个call方法执行  this->B.call.call(就是个call方法)  context->A  params->[20,10]
// “A[key]”=call方法  “A[key]”(20,10) 
// -> 继续把“call方法执行”   this->A  context->20  params->[10]
//  “new Number(20)[key]” = A;   “new Number(20)[key]”(10)
// 让A执行,A中的this指向20,传递实参10  => NaN undefined

Function.prototype.call(A, 60, 50);
// 直接最后:把Function.prototype执行(匿名空函数),让其中的this指向A,传递60/50  => 无输出

Function.prototype.call.call.call(A, 80, 70);
// “Function.prototype.call.call” -> “call方法 @CA”
// 最后一个CALL执行:让@CA执行,让里面的this是A,传递80/70
// 80.xxx = A;  80.xxx(70);
//  “new Number(80)[key]” = A;   “new Number(80)[key]”(70)
// 把A执行,A中的this是80,传递实参70 => NaN undefined

// 规律:
// 一个CALL执行 例如:B.call(A, 40, 30);
// => 让B执行,让B中的this是A,给B传递40/30
// 多个CALL执行 例如:B.call.call.call(A, 20, 10);
// => 让A执行,让A中的this是20,给A传递10

专题:JQ核心源码阅读分析

JQ选择器:$([selector],[context])

[context]默认值document

  • 每一次选择器执行都是创造jQuery构造函数的一个实例「私有属性方法、找到jQuery.prototype上的公共属性方法」
    • jQuery实例对象
    • JQ对象
    • 一般都是一个“类数组集合”:集合中每一项存储的是获取的DOM元素对象(原生)、length代表集合长度

[selector]

  • “假”值,例如:""、null、undefined、false、0,返回一个空实例对象(不具备任何的私有属性)
  • 函数:(function())等价于(function(){}) 等价于 (document).ready(function(){}) 当DOM结构加载完成(DOM TREE生成,监听DOMContentLoaded事件),触发回调函数执行
  • DOM节点:给实例设置索引0,属性值是这个DOM节点,设置length属性为1,目的:把原生DOM元素对象转换为JQ对象(应用JQ原型上的方法)
  • 字符串:
    • HTML字符串:动态创建DOM元素对象,再把其变为一个JQ对象(索引零存储这个DOM对象)返回
    • ID选择器:基于getElementById获取DOM对象,转换为JQ对象返回
    • 其它选择器:则按照JQ内部选择器的解析方案(jQuery.find),获取当前指定上下文中,所有和选择器匹配的内容,最后返回JQ对象
  • 其它类型:
    • 数组或者类数组集合:会和空的JQ对象进行合并,最后返回包含传递集合中每一项的JQ对象
    • 如果是其它值:把传递的值放在JQ对象的末尾
(function () {
    function jQuery(selector, context) {
        return new jQuery.fn.init(selector, context);
    }
    jQuery.fn = jQuery.prototype = {
        constructor: jQuery,
        //...
    };

    // 中间转换
    function init(selector, context, root) {}
    jQuery.fn.init = init;
    init.prototype = jQuery.fn;


    if (typeof window !== "undefined") {
        window.$ = window.jQuery = jQuery;
    }
})();
// $() -> jQuery实例 

Xnip2021-12-12_10-39-43

把JQ对象转换为原生DOM对象(目的:JS内置的属性方法)

  • JQ对象[索引]
  • JQ对象.get([索引])
  • JQ对象.eq([索引]) :也支持负数索引、获取JQ集合中的某一项,但是最后是以一个新的JQ对象返回,prevObject属性记录原始JQ集合
$('.box');
$(function () {
    // DOM结构已经渲染完,在这里可以获取页面中的DOM元素
});
document.body.addClass('box'); //Uncaught TypeError: document.body.addClass is not a function  addClass是jQuery.fn上的方法,只有JQ对象才可以调用
$(document.body).addClass('box');
(function (global, factory) {
    "use strict";
    if (typeof module === "object" && typeof module.exports === "object") {
        module.exports = global.document ?
            factory(global, true) :
            function (w) {
                if (!w.document) {
                    throw new Error("jQuery requires a window with a document");
                }
                return factory(w);
            };
    } else {
        factory(global);
    }
})(typeof window !== "undefined" ? window : this, function (window, noGlobal) {
    "use strict";
    var version = "3.6.0",
        jQuery = function (selector, context) {
            return new jQuery.fn.init(selector, context);
        };

    jQuery.fn = jQuery.prototype = {
        jquery: version,
        constructor: jQuery,
        length: 0,
        get: function (num) {
            // num:undefined/null 把JQ对象类数组集合转换为数组集合
            if (num == null) return slice.call(this);
            // 支持负数索引
            return num < 0 ? this[num + this.length] : this[num];
        },
        eq: function (i) {
            var len = this.length,
                j = +i + (i < 0 ? len : 0);
            return this.pushStack(j >= 0 && j < len ? [this[j]] : []);
        },
        pushStack: function (elems) {
            var ret = jQuery.merge(this.constructor(), elems);
            ret.prevObject = this;
            return ret;
        },
    };

    var rootjQuery = jQuery(document),
        rquickExpr = /^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,
        init = jQuery.fn.init = function (selector, context, root) {
            var match, elem;
            if (!selector) return this;
            root = root || rootjQuery;
            if (typeof selector === "string") {
                if (selector[0] === "<" &&
                    selector[selector.length - 1] === ">" &&
                    selector.length >= 3) {
                    match = [null, selector, null];
                } else {
                    match = rquickExpr.exec(selector);
                }
                if (match && (match[1] || !context)) {
                    // ID选择器或者HTML字符串
                    if (match[1]) {
                        // HTML字符串
                        context = context instanceof jQuery ? context[0] : context;
                        jQuery.merge(this, jQuery.parseHTML(
                            match[1],
                            context && context.nodeType ? context.ownerDocument || context : document,
                            true
                        ));
                        if (rsingleTag.test(match[1]) && jQuery.isPlainObject(context)) {
                            for (match in context) {
                                if (isFunction(this[match])) {
                                    this[match](context[match]);
                                } else {
                                    this.attr(match, context[match]);
                                }
                            }
                        }
                        return this;
                    } else {
                        // ID选择器
                        elem = document.getElementById(match[2]);
                        if (elem) {
                            this[0] = elem;
                            this.length = 1;
                        }
                        return this;
                    }
                } else if (!context || context.jquery) {
                    // 没有设置上下文,或者设置的上下文是一个JQ对象
                    return (context || root).find(selector);
                } else {
                    // 设置了上下文,但是不是JQ对象
                    return this.constructor(context).find(selector);
                }
            } else if (selector.nodeType) {
                this[0] = selector;
                this.length = 1;
                return this;
            } else if (isFunction(selector)) {
                return root.ready !== undefined ?
                    root.ready(selector) :
                    selector(jQuery);
            }
            return jQuery.makeArray(selector, this);
        };
    init.prototype = jQuery.fn;

    //------设置的静态私有属性方法
    jQuery.makeArray = function (arr, results) {
        var ret = results || [];
        if (arr != null) {
            if (isArrayLike(Object(arr))) {
                jQuery.merge(ret,
                    typeof arr === "string" ?
                        [arr] : arr
                );
            } else {
                push.call(ret, arr);
            }
        }
        return ret;
    };
    // 合并两个集合(可以是数组也可以是类数组):把第二个集中的每一项插入到第一个集合的末尾,最后返回第一个集合
    jQuery.merge = function (first, second) {
        var len = +second.length,
            j = 0,
            i = first.length;
        for (; j < len; j++) {
            first[i++] = second[j];
        }
        first.length = i;
        return first;
    };
    jQuery.each = function (obj, callback) {
        var length, i = 0;
        if (isArrayLike(obj)) {
            length = obj.length;
            for (; i < length; i++) {
                if (callback.call(obj[i], i, obj[i]) === false) {
                    break;
                }
            }
        } else {
            for (i in obj) {
                if (callback.call(obj[i], i, obj[i]) === false) {
                    break;
                }
            }
        }
        return obj;
    };

    //------ready
    var readyList = jQuery.Deferred();
    jQuery.fn.ready = function (fn) {
        readyList
            .then(fn)
            .catch(function (error) {
                jQuery.readyException(error);
            });
        return this;
    };
    jQuery.ready.then = readyList.then;

    /* 暴露API */
    if (typeof define === "function" && define.amd) {
        define("jquery", [], function () {
            return jQuery;
        });
    }
    if (typeof noGlobal === "undefined") {
        window.jQuery = window.$ = jQuery;
    }
    return jQuery;
});

迭代方法:迭代数组、类数组、对象(支持回调函数返回false结束循环)

(function () {
    "use strict";
/* 迭代方法:迭代数组、类数组、对象(支持回调函数返回false结束循环) */
    const each = function each(obj, callback) {
        if (obj == null || !/^object$/.test(typeof obj)) throw new TypeError("obj must be an object/array/likeArray");
        if (typeof callback !== "function") throw new TypeError("callback is not a function");
        let item, keys, key;
        if (isArrayLike(obj)) {
            for (let i = 0; i < obj.length; i++) {
                item = obj[i];
                if (callback.call(item, item, i) === false) break;
            }
        } else {
            keys = Object.getOwnPropertyNames(obj);
            if (typeof Symbol !== "undefined") keys = keys.concat(Object.getOwnPropertySymbols(obj));
            for (let i = 0; i < keys.length; i++) {
                key = keys[i];
                item = obj[key];
                if (callback.call(item, item, key) === false) break;
            }
        }
        return obj;
    };
    /* 暴露API */
    const utils = {
        version: '1.0.0',
        each,
    };
    if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
    if (typeof window !== "undefined") window.utils = utils;
})();

专题:数组对象的深浅拷贝和深浅合并

深浅拷贝

  • 堆栈内存
  • 深拷贝 VS 浅拷贝
  • 哪些对象不能用JSON深克隆?
    • 不允许出现套娃操作
    • 属性值不能是BigInt
    • 丢失一些内容:只要属性值是 symbol/undefined/function 这些类型的
    • 还有信息不准确的,例如:正则->空对象 Error对象->空对象 日期对象->字符串 …

Xnip2021-12-12_14-40-58

深拷贝:可以基于JSON.stringify/parse来处理

原理:先把对象变为字符串,再把字符串重新变为对象「浏览器会重新开辟所有需要的堆内存」

let obj = {
    name: '珠峰',
    age: 13,
    bool: true,
    n: null,
    u: undefined,
    sym: Symbol('sym'),
    // big: 10n,
    list: [10, 20, 30],
    reg: /\d+/,
    time: new Date,
    err: new Error('xxx'),
    ke: {
        js: '基础课',
        web: '高级课'
    },
    [Symbol('KEY')]: 100,
    fn: function () { }
};
obj.obj = obj;

let obj2 = utils.clone(obj, true);
console.log(obj2);

//console.log(JSON.stringify(obj));
//console.log(JSON.parse('{"name":"珠峰","age":13,"ke":{"js":"基础课","web":"高级课"},"list":[10,20,30]}'));
let deepObj = JSON.parse(JSON.stringify(obj));
console.log(deepObj, obj);
console.log(deepObj === obj); //false
console.log(deepObj.ke === obj.ke); //false 

JSON.stringify处理深拷贝,存在的局限性:

  • 无法处理BigInt类型的属性值 Uncaught TypeError: Do not know how to serialize a BigInt
  • 某些键值对会消失
    • 属性值是 undefined/symbol/function 类型的
    • 属性名是 symbol 类型的
    • 属性值如果是 正则对象/错误对象 会转换为“{}”,值和之前是不一样的
    • 日期对象变为字符串之后就无法在转换回日期对象了
    • 一但内部出现“套娃操作”(obj.obj=obj),直接处理不了 Uncaught TypeError: Converting circular structure to JSON
    • ...
  • 处理起来没有问题的类型:number、string、boolean、普通对象、数组对象...
(function () {
    "use strict";
/* 数组/对象的深浅拷贝&深浅合并 */
    const clone = function clone(obj, deep, exist) {
        if (obj == null) return obj;
        if (typeof deep !== "boolean") deep = false;
        let ctor = obj.constructor,
            type = toType(obj),
            isArray = Array.isArray(obj),
            isObject = isPlainObject(obj),
            result;
        // 其他类型值的处理
        if (/^(regexp|date)$/i.test(type)) return new ctor(obj);
        if (/^(error)$/i.test(type)) return new ctor(obj.message);
        if (typeof obj === "function") {
            return function (...params) {
                return obj.call(this, ...params);
            };
        }
        if (!isArray && !isObject) return obj;
        // 避免套娃出现死递归
        if (!Array.isArray(exist)) exist = [];
        if (exist.indexOf(obj) > -1) return obj;
        exist.push(obj);
        // 如果是数组&纯粹对象
        result = new ctor();
        each(obj, (value, key) => {
            if (deep) {
                result[key] = clone(value, deep, exist);
                return;
            }
            result[key] = value;
        });
        return result;
    };
    /* 暴露API */
    const utils = {
        version: '1.0.0',
        clone,
    };
    if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
    if (typeof window !== "undefined") window.utils = utils;
})();

深浅合并

**Object.assign:**分别把obj2/obj3合并到obj1中,obj1堆内存中的内容被修改,最后返回的也是obj1「浅合并,只对第一级进行比较合并,出身更深层级,直接把之前替换掉即可(整体替换)」

  • obj1有、obj2有:以obj2为主
  • obj1有、obj2没有:--
  • obj1没有、obj2有:给obj1新增一个这个属性
let obj = Object.assign(obj1, obj2, obj3);
obj = Object.assign({}, obj1, obj2, obj3); //这样不修改任何的原始对象

$.extend()

// $.fn.extend(obj):向JQ原型上扩展属性方法
// $.extend(obj):向JQ对象上扩展属性方法
//   this -> 要被替换的对象
//   target -> obj替换this的对象
// $.extend(obj) 浅合并  deep=false  target=$  i=0  options=obj
// $.extend(true,obj) 深合并  deep=true  target=$  i=1  options=obj
// $.extend(true,obj1,obj2,obj3) 多项深合并(和JQ对象没关系,上述操作都是把obj替换JQ对象)  deep=true  target=obj1  i=2 options=obj2/obj3... 让其分别替换obj1
jQuery.extend = jQuery.fn.extend = function () {
    var options, name, src, copy, copyIsArray, clone,
        target = arguments[0] || {},
        i = 1,
        length = arguments.length,
        deep = false;
    if (typeof target === "boolean") {
        deep = target;
        target = arguments[i] || {};
        i++;
    }
    if (typeof target !== "object" && !isFunction(target)) {
        target = {};
    }
    if (i === length) {
        target = this;
        i--;
    }
    for (; i < length; i++) {
        options = arguments[i];
        if (options != null) {
            for (name in options) {
                copy = options[name];
                if (name === "__proto__" || target === copy) {
                    continue;
                }
                if (deep && copy && (jQuery.isPlainObject(copy) ||
                    (copyIsArray = Array.isArray(copy)))) {
                    src = target[name];
                    if (copyIsArray && !Array.isArray(src)) {
                        clone = [];
                    } else if (!copyIsArray && !jQuery.isPlainObject(src)) {
                        clone = {};
                    } else {
                        clone = src;
                    }
                    copyIsArray = false;
                    target[name] = jQuery.extend(deep, clone, copy);
                } else if (copy !== undefined) {
                    target[name] = copy;
                }
            }
        }
    }
    return target;
};

utils.merge

let obj1 = {
    name: '张三',
    age: 25,
    hobby: {
        music: 100,
        jump: 80
    }
};
obj1.obj1 = obj1;

let obj2 = {
    name: '李四',
    age: 22,
    sex: 0,
    hobby: {
        read: 100,
        music: 90
    }
};
let obj3 = {
    name: '王五',
    age: 20,
    height: '158cm',
    score: {
        math: 100,
        chinese: 90
    }
};

let obj = utils.merge(true, {}, obj1, obj2, obj3);
console.log(obj);
(function () {
    "use strict";
const merge = function merge() {
        let options,
            target = arguments[0] || {},
            i = 1,
            length = arguments.length,
            exist = arguments[length - 1],
            deep = false;
        if (typeof target === "boolean") {
            deep = target;
            target = arguments[i] || {};
            i++;
        }
        if (target == null || (typeof target !== "object" && !isFunction(target))) target = {};
        // 防止死递归
        Array.isArray(exist) && exist.isExist ? length-- : (exist = [], exist.isExist = true);
        for (; i < length; i++) {
            options = arguments[i];
            if (options == null) continue;
            if (exist.indexOf(options) > -1) return options;
            exist.push(options);
            each(options, (copy, name) => {
                let copyIsArray = Array.isArray(copy),
                    copyIsObject = isPlainObject(copy),
                    src = target[name];
                if (deep && copy && (copyIsArray || copyIsObject)) {
                    if (copyIsArray && !Array.isArray(src)) src = [];
                    if (copyIsObject && !isPlainObject(src)) src = {};
                    target[name] = merge(deep, src, copy, exist);
                } else if (copy !== undefined) {
                    target[name] = copy;
                }
            });
        }
        return target;
    };
    /* 暴露API */
    const utils = {
        version: '1.0.0',
        merge,
    };
    if (typeof module === "object" && typeof module.exports === "object") module.exports = utils;
    if (typeof window !== "undefined") window.utils = utils;
})();