[面向对象]
目录
[TOC]
什么是面向对象?
自然界中的所有东西都是我们需要研究的对象,分类:大类,小类,按照相似的功能特点,拿出人类中的一个人,去研究分析,人和人之间都有自己独有的特性 公用特征
JS是面向对象编程,JS中的每一个东西,都是我们需要研究的"对象"泛指,按照功能特点进行分"类",拿出类中的某个事物,称之为实例
JS中的内置类:(专业叫法:构造函数)
- js中有很多的内置类,每一种数据类型都有自己所属的内置类,Object是所有类的基类(大类)
基本值类型
- Number:每一个数字都是Number的实例
- String: 每一个字符串都是String的实例
- Boolean:每一个布尔值都是Boolean的实例
- Symbol:每一个唯一值都是Symbol的实例
- BigInt:每一个最大安全整数都是BigInt的实例
复杂数据类型
- Object 每一个对象都是Object的实例 (普通对象, 特殊对象,实例对象,原型对象,函数对象)
- Function每一个函数都是Function的实例 ( 普通函数,自定义.内置构造函数 箭头函数)
- Array 每一个数组都是Array的实例
- RegExp 每一个正则都是RegExp的实例
- Date 每一个日期对象都是Date的实例
- 每一个HTML标签(或者每一个节点),都有自己的内置类
body-->HTMLBodyElement>HTMLElement--->Element---->Node--->EventTarage--->Object
Node:节点的小类(Element:元素节点 ,Text文本节点, Document文档节点,Comment注释节点)
Element 元素类的小类(XMLElement, HTMLElement HTML 旨在显示信息,而 XML 旨在传输信息)
3.元素集合类
元素集合类HTMLCollectior( document.getElementByTagName)
节点集合类NodeList(document.querySelectorAll)
自定义类&& 普通函数执行和构造函数执行的区别
创建自定义类
创建一个函数,执行的时候把其new执行,则当前函数被称作为自定义构造函数(类),执行的返回结果一般是当前类的一个实例 new多次会创建出多个不同的实例对象,(实例对象独立性):基于this.xxx=xxx是给实例对象设置私有属性和方法
普通函数执行
形成函数私有上下文, 初始化作用域链.初始化this,初始化arguments,形参赋值,变量提升,代码执行
构造函数执行
构造函数执行,其中大部分操作和普通函数执行一样 形成函数私有上下文 ,
new fn(10,20)&&new fn 都把fn执行了 优先级问题:带小括号优先级高,不带小括号优先级低
区别
- 1.初始化作用域链之前浏览器会默认创建一个对象(空对象) 是fn的实例对象
- 2.初始化this的时候会让this指向这个实例对象,所以后期在代码执行的时候,this.xxx=xxx都是给实例对象设置的私有属性和方法(私有变量和实例没有关系)
- 3.在代码执行完,看函数本身的返回结果:返回的是对象类型的值return 对象则以自己返回的为主,不写返回值,或者返回的是个原始值,则浏览器默认会把创建的实例对象返回
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); //(私有变量和实例没有关系) undefined
console.log(f1.total); //30
console.log(f1.say === f2.say); //false 每一个实例都是私有的
图解
原型&&原型链查找机制
prototype(存储公共的属性和方法)
大部分“函数数据类型”的值都具备“prototype(原型/显式原型)”属性,属性值本身是一个对象「浏览器会默认为其开辟一个堆内存, 用来存储实例可调用的公共的属性和方法」 ,在浏览器默认开辟的这个堆内存中「原型对象」有一个默认的属性“constructor(构造函数/构造器)”,属性值是 当前函数/类本身!!
注意:<Function.prototyoe是一个匿名函数,而其余的构造函数的原型对象都是对象,虽说是个函数,但是他和其他原型对象操作相同>
公有&&私有属性/方法
说属性/方法是公有的还是私有的是相对的
Fn.prototype-->原型对象, f1实例对象(如下面代码getY相对于原型对象来讲是它自己私有的相对于实例对象来说其公有方法)
函数数据类型
- 普通函数(实名或者匿名函数)
- 箭头函数
- 构造函数/类「内置类/自定义类」
- 生成器函数 Generator
不具备prototype的函数
- 箭头函数
- 基于ES6给对象某个成员赋值函数值的快捷操作 fn2(){}
prototype和__proto__(原型链/隐式原型)
每一个“对象数据类型”的值都具备一个属性“proto(原型链/隐式原型)”,属性值指向“自己所属类的原型prototype”对象,在浏览器中叫做[[Prototype]]
原型链查找机制
- 所有对象都是Object内置类的实例,所以说_protp_不论如何查找,都能找到Object.prototype上,
- Object.prototype对象上也有__protp__如果有指向则指向"自己",所以此处的__protp__===null
- 进行成员访问时 :对象.xxx先看自己私有属性和方法,不是私有的则基于__protp__找所属类的prototype上的属性和方法,如果在没有则基于原型对象上的__proto__向上查找,直到找到,Object.prototype为止 原型链的查找机制
- 对象__proto__.xxxx或者构造函数.Prototype.xxx都是跳过私的有查找,直接找到原型对象上公共的属性和方法
对象数据类型值
- 普通对象
- 特殊对象:数组、正则、日期、Math、Error…
- 函数对象
- 实例对象
- 构造函数.prototype
function Fn() {
//实例私有的属性和方法
this.x = 100;
this.y = 200;
this.getX = function () {
console.log(this.x);
}
}
//供实例调用的公有属性和方法(相对于prototype对象来说是自己私有的)
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);//false 都是自己私有的
console.log(f1.getY === f2.getY);//true 自己没有,都是Fn原型对象上公有方法
//对象__proto__.xxxx或者构造函数.Prototype.xxx都是跳过私的有查找,直接找到原型对象上公共的属性和方法
console.log(f1.__proto__.getY === Fn.prototype.getY);/true
console.log(f1.__proto__.getX === f2.getX);//flse f1找公有的,f2找私有的
console.log(f1.getX === Fn.prototype.getX);//false f1是私有的
console.log(f1.constructor);//Fn
console.log(Fn,.prototype.__proto__.constructor);//Object
f1.getX();//100, 确定查找方法,确定this
f1.__proto__.getX();//公有. this--->f1__proto__, console.log(f1__proto__(x))undefined
f2.getY(); //公有 this->f2, console.log(f2.y) 200
Fn.prototype.getY(); 公有, this-->Fn.prototype console.log(Fn.prototype.y) undefined
图解
Function & Object 之间的爱恨情仇
把Array函数当做对象设置的静态属性方法,和实例没有关系,一般都会编写一些工具类方法
- Array.rom,(把类数组转为数组)
- Array.isArray,(检测是不是数组)
- Array.of()方法用于将一组数值转换为数组
- prototype :把Array当做一个构造函数,属性值是一个对象,(提供实例使用的公共属性方法)和实例有关系,arr.xxx()或Array.prototype.xxx()
- name ,length,
内置类原型指向图:
Object 每一个对象都是Object的实例 (普通对象, 特殊对象,实例对象,原型对象,函数对象),每一个对象上都有[[Prototype]],指向所属类的原型对象
- arr -> Array.prototype -> Object.prototype,---->null
- fn-->Function.prototype-->Object.prototype,--->null
Function每一个函数都是Function的实例 ( 普通函数,自定义.内置构造函数 箭头函数),Array,Object作为内置构造函数都是函数的实例
- Array---->Function.prototype-> Object.prototype,---->null
- Function---->Function.prototype-> Object.prototype,---->null
- Object--->Function.prototype-> Object.prototype,---->null
- 最后都会找到Object.prototype,Object是所有对象的"基类",所有对象的__proto__最终都会止步于Object.prototype
函数的三种角色 普通函数,构造函数,普通对象
总结
- Function.prototype===Function__proto__:说明Function(函数)是Function类的实例
- Function.proto.proto===Object.prototype:说明Function(对象)是Object的实例
- Object__proto__===Function.prototype:Object(函数),是Function类的实例
- Object__proto__.proto===Object.prototype:说明Object(对象)是Object的实例
图解
在内置类原型自定义(或扩展)属性和方法供其实例调取使用
- 优势:操作起来更贴近于"原生"的操作
- 方法中的this一般都是当前类的实例,直接操作即可(不需要额外判断数据类型)
- 可以实现链式写法:方法执行完返回的结果, 依然是 当前类的实例,这样可以继续调用其他方法进行操作了
- [特殊]:我们自己扩展的方法,最后设置前缀例如 myXxx,这样可以防止自己写的方法覆盖了内置的属性和方法,导致不可控的Bug
基于内置类原型上扩展去重方法
//数组去重
let arr = [2, 1, 3, 2, 1, 3]
new Set(arr)
//构造一个没有重复的项[[Entries]]set数据结构
console.log(new Set(arr));
//Array.from把伪数组转成一个数组
let newArr = Array.from(new Set(arr))
-------------------------------------------
//普通方法是执行方法传递参数
const unique = function (arr) {
//首先校验arr值类型的合法性
return Array.from(new Set(arr))
}
let arr = [2, 1, 3, 2, 1, 3]
arr = unique(arr)
arr.sort((a, b) => a - b)//原型上的方法可以直接调取使用
console.log(arr);
---------------------------------------
// 在原型上扩展方法
Array.prototype.unique = function unique() {
// this--->实例arr
return Array.from(new Set(this))
}
let arr = [2, 1, 3, 2, 1, 3];
//反会的数组是一个新数组,可以继续调方法(链式写法)
arr = arr.unique().sort((a, b) => a - b)
面试题—Number方法重构
//完成如下需求
let n = 10;
let m = n.plus(10).minus(5);
console.log(m);//=>15(10+10-5)
只需要重写Number原型上的plus、minus这两个方法即可:
Number.prototype.plus=function plus(val){
//这里的this指向是n
return this+val;
};
Number.prototype.minus=function minus(val){
//这里的this指向是n
return this-val;
};
let n = 10;
let m = n.plus(10).minus(5);
console.log(m);//=>15(10+10-5)
//输出结果为15
Object.prototype.hasOwnProperty 用来检测是否为私有属性
语法:(对象).hasOwnProperty(属性):检测(属性)是否为(对象)的私有属性 是返回true,不是返回false;只看私有中有没有(和公有是否存在没有关系)
私有属性还是公有属性本来就是相对的概念(自己堆内存中的是私有属性是__proto__查找的"相对自己"是公有属性)
let arr = [10, 20];
相对于arr来push是公有方法
console.log(arr.hasOwnProperty('push'));false
//相对于Array来说,push方法是自己的私有属性
console.log(Array.prototype.hasOwnProperty('push'));//true
arr.push(30);//arr首先找自己私有属性发现没有push方法,则,,默认基于__proto__找arr.prototype上找,所以它是找到Array.prototype.push方法执行(this-->arr实参-->30)而push方法执行的作用是给arr(this)的末尾追加新的内容30,并且让数组长度累加1返回新增后的数组长度
in操作符
- 语法: [属性]in[对象] 检测属性是否属于这个对象,不论是公有还是私有,只要能访问到这个属性,则结果就是true
- console.log('push' in arr);//true
面试题:封装方法hasPubProperty:用来检测当前属性是否为对象的公有属性(无关私有中是否存在)
Object.getPrototypeOf(实例对象),获取当前实例对象的原型对象(或者获取 实例对象的__protp__)
有可能没有原型对象 Object.create() 方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。let x=Object.create(null) Object.getPrototypeOf(x)没有原型对象
<script>
Object.prototype.hasPubProperty = function hasPubProperty(attr) {
// this-->obj要处理的对象 attr-->toString我们要检测的属性
// //思路1. 是对象的属性,而且还不是私有属性,这眼就只能是公有属性了
// //问题:如果attr即是私有属性,也是公有属性,基于这种方案检测结果为false
// return (attr in this) && !this.hasOwnProperty(attr)
//-------------------------------------------------
// 思路二:跳过私有属性查找,直接在公有属性上查找,看看是否存在
let proto = Object.getPrototypeOf(this);
// 循环如果有原型对象就一直往上找,直到为null
while (proto) {
//如果Object原型对象上有私有属性,那就是实例属性的公有属性
if (proto.hasOwnProperty(attr)) return true;
proto = Object.getPrototypeOf(proto)
}
//没有原型对象
if (!proto) return false;
};
let obj = {
name: 'Lisa',
age: 18,
toString() { }
};
//检测toString是不是obj的公有属性 是为true 不是为false
console.log(obj.hasPubProperty('toString'));
Object.create(null)
基于ES6中的CLASS重构下面的代码
es5
function Modal(x, y) {
//构造函数体 this-->实例
//这是给实例设置的私有属性方法
this.x = x;
this.y = y;
}
//把Modal作为一个构造函数在其原型对象上供实例调用的公共属性和方法
Modal.prototype.z = 10;
Modal.prototype.getX = function () {
console.log(this.x);
}
Modal.prototype.getY = function () {
console.log(this.y);
}
//把Modal作为普通对象,设置的静态私有属性和方法
Modal.n = 200;
Modal.setNumber = function (n) {
this.n = n;
};
let m = new Modal(10, 20);
es6
- 基于class语法设置的原型上的公共方法, (或静态私有方法)都是不可枚举的属性,但是基于xxx=xxx 这种写法,设置的属性是可枚举的
- 基于class语法,创建的构造函数, 是不能当做普通函数执行的,不带new会报错
class ModalB {
//构造函数体:两种写法都是给实例设置私有的属性方法
constructor(x, y) {
this.x = x;
this.y = y;
this.sum = function () { }
}
z = 10;
sum = function sum() { }
//在原型对象上设置供实例调用的 "公共方法",基于class语法,无法向原型上直接设置公有属性
getX() { console.log(this.x); }//不可枚举的属性
getY() { console.log(this.y); }
//把其作为普通对象,设置静态私有属性方法
static n = 200;
static setNumber(n) { this.n += n }
}
//向原型扩展公共属性
ModalB.prototype.age = 18
let n = new ModalB(10, 20);
console.log(n);
console.dir(ModalB)
面向对象进阶[重写内置new方法]
面试题1
new Foo && new Foo()
- 都是把Foo执行,创建Foo的实例
- new Foo 无参数列表new 优先级19
- new Foo() 带参数类别new 可以传递实参信息 优先级20
- obj.name成员访问优先级20
- 优先级相同从左到右运算,优先级不同先算高
functionFoo(){
//执行时 getName 是全局的
getName = function () {
console.log(1);
};
return this;
}
//把Foo当做普通对象设置静态私有属性方法
Foo.getName = function () {
console.log(2);
};
把Foo当做构造函数执行,向其原型对象上设置一些供实例调用的属性和方法
Foo.prototype.getName = function () {
console.log(3);
};
var getName = function () {
console.log(4);
};
function getName() {
console.log(5);
}
Foo.getName();// 2 访问Foo的静态私有方法
getName();// 4 全局的getName函数执行
Foo().getName(); // 1 先把Foo当做普通函数执行,执行的返回结果在调用getName ==>window.getName
getName();//1 全局的getName函数执行
new Foo.getName();// 2 Foo.getName 2 new function==>2()当做普通函数执行
new Foo().getName();/ 3 /从左到右 实例.getName()没有实例私有属性,找到原型输出3
new new Foo().getName(); 3//从右到左new 实例.getName()-->成员访问优先级高new function-->3()
图解(总结)
函数具备多种角色
- 普通函数:私有上下文,私有变量, 作用域,作用域链,闭包
- 构造函数,具备普通函数的特性,类,实例,prototype,proto
- 普通对象:键值对
面试题2
假如没有new关键词,需要我们自己编写new方法,实现和new相同的效果
分析:
1.创建当前类Ctor的一个实例对象(空对象 ,proto===Ctor.prototype)
2.把构造函数当做普通函数执行,只不过让函数中的this指向创建的实例对象
3.判断函数的返回结果,如果返回的是个对象,则以自己返回的结果为主,否则把创建的实例对象返回
<script>
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();
*/
//假如没有new关键词,需要我们自己编写new方法,实现和new相同的效果
function _new(Ctor, ...params) {
// ctor == construction指回构造函数 创建Dog的实例,...params传递的参数不确定
// // 创建当前类Ctor的一个实例对象(空对象 ,__proto__===Ctor.prototype)
// 方案1.__proto__在ie中除了(EDGE)其余的都不允许访问
// let obj = {}
// obj.__proto__ = Ctor.prototype
// 方案2.给某个对象设置原型指向(也就是让obj.__proto__===Ctor.prototype),只兼容ie11及以上
// let obj = {};
// Object.setPrototypeOf(obj, Ctor.prototype)
//方案3, Object.create(proto)创建一个空对象,并把proto作为空对象的原型指向(空对象.__proto__===proto)proto必须是个对象,或者null,如果是null则是创造一个没有原型指向的对象,不兼容ie 678
let obj = Object.create(Ctor.prototype)
//.把构造函数当做普通函数执行,只不过让函数中的this指向创建的实例对象
let result = Ctor.call(obj, ...params)//把数组展开分别传给Ctor
//判断函数的返回结果,如果返回的是个对象,则以自己返回的结果为主,否则把创建的实例对象返回
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,用来检测某个对象是否是当前构造函数的实例对象
</script>
最终版&&兼容版
最终版
内置new有特殊情况,
- 箭头函数没有prototype,不能被new
- 内置构造函数,symbol,BigInt不能被new
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
}
兼容版
...prams不兼容,可以利用arguments类数组接收所有形参
铺垫arguments&&Array.prototype.slice
arguments类数组不能使用数组方法
Array.from 把argumens变为数组集合,Es6新增,IE不兼容
function fn() {
// 类数组不能直接使用Array.prototype上的方法
// console.log(arguments);
// Uncaught TypeError: arguments.slice is not a function
// console.log(arguments.slice(1));
// let arg = Array.from(arguments)
// console.log(arg);Es6新增,IE不兼容
var arg = [];
for (var i = 0; i < arguments.length; i++) {
arg.push(arguments[i])
}
console.log(arg);
}
fn(10, 20, 30, 40, 50)
基于Array.prototype.slice实现数组克隆
模拟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, 50]
let arr2 = arr.slice()
console.log(arr2);
总结: 通过对比我们发现:只要把Array.prototype.slice方法执行让方法中的this指向arguments就相当于循环arguments中的每一项,并且赋值给新数组,实现把类数组转换为数组(前提:类数组集合和数组结构基本一致,只不过是不能直接使用slice方法而已)
- 把slice执行 Array.prototype.slice()或者[].slice()
- 改变this call方法
- 类数组借用数组上的方法进行操作(大部分方法都可以基于这种方式借用)[].xxx.call ([类数组],(实参...))
function fn() {
var arg = [].slice.call(arguments)
console.log(arg);
}
fn(10, 20, 30, 40, 50)
兼容版
fn.call(obj,10,20,30)传参时一项一项的传
fn.apply([10,20,30])直接传递一个数组
function _new(Ctor) {
if (typeof Ctor !== "function") throw new TypeError('Ctor is not a constructor')
if (!Ctor.prototype || Ctor === Symbol || Ctor === BigInt) throw new TypeError('Ctor is not a constructor')
var obj,
result,
proto = Ctor.prototype,
params;
params = [].slice.call(arguments, 1)
console.log(params);
obj = Object.create(proto)
result = Ctor.apply(obj, params)
if (result !== null && /^(object|function)$/.test(typeof (result))) return result
return obj
}