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![]()
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);
函数区别:
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给对象某个成员赋值函数值的快捷操作
- ....
每一个“对象数据类型”的值都具备一个属性“_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();
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
/* 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
- in操作符
- 面试题:在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);
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);
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);
@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();
8、函数在__proto__和prototype之间的关系
9、进阶:重写内置NEW
面试题:假如没有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); //=>trueObject.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); //falseclass 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();
面试题
/* * 编写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; }
鸭子数据类型的几种常规处理
鸭子类型:所代表的是那些“类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,返回一个空实例对象(不具备任何的私有属性)
- 函数:(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实例
把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对象->空对象 日期对象->字符串 …
深拷贝:可以基于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); //falseJSON.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; })();