面向对象和类的定义
类:所谓“类”就是对象的模板,对象就是“类”的实例。对象通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成
那么,“对象”(object)到底是什么?我们从两个层次来理解。 (1)对象是单个实物的抽象。 一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。 (2)对象是一个容器,封装了属性(
property)和方法(method)。 属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。
JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor) 和 原型链(prototype)
以上概念来自阮一峰JavaScript教程-面向对象编程
开发示例:如果一个页面有两个轮播图,我想让这两个轮播图各自单独运行,但是核心的代码逻辑不变,且两个实例互不影响,他们的核心运行逻辑和表现都一样,但是需要各自单独运行他们自己的逻辑,且有自己的数据和状态,这时候就要用到面向对象的开发方法。插件封装组件封装,都是面向对象的思想的应用,都是对私有化和公有化的合理管控。
JS本身就是面向对象的编程语言,而他本身也是基于面向对象的思想构建出来的语言。
面向对象思想:
- 对象:万物皆对象
- 类:对“对象”的归类和细分
- 实例:以类为模板创造出来的具体成员
内置类
JS中的类:内置类、自定义类
内置类:
- 每一种数据类型有自己对应的内置类:
Number、String、Boolean、Symbol、BigInt、Object(Array、RegExp、Date、Set、Map、ArrayBuffer...)、Function - 我们平时接触的具体值就是对应的类的实例,例如:
10就是Number类的一个实例,[10,20] ->Array->Object... - 每一个HTML元素标签「包含
window/document等」在JS中都有自己的内置类,例如:div->HTMLDivElement->HTMLElement->Element->Node->EventTarget->Object
一个DOM元素对象
在封装,抽象的时候,要始终记得使用面向对象的思想
自己创造类
普通函数执行
function Fn(x, y) {
let total = x + y,
flag = false;
this.total = total;
this.say = function say() {
console.log(this.total);
};
}
let result = Fn(10, 20);
console.log(result);//undefined
普通函数执行,函数中的this要根据函数运行时所在的环境决定
构造函数执行
let result1 = new Fn(10, 20);
let result2 = new Fn(30, 40);
console.log(result1.say === result2.say); //->false
使用new代表把函数当作构造函数执行,这个构造函数就是自定义的类
Fn被称为类或者构造函数result被称为当前类的一个实例
使用new关键字执行构造函数比普通函数执行做了如下操作:
- 在初始化
this之前,默认创建一个空对象(作为返回的实例对象),在初始化this的时候,将this指向创建的实例对象 - 所以在代码执行阶段,函数体中遇到
this.xxx = xxx的赋值操作,都是给实例对象设置私有的属性或方法(上下文中的私有变量和实例对象没有直接关系,只有遇到类似this.xxx = xxx的赋值,才有关系) - 如果函数不设置返回值或者返回的是原始值类型,则默认返回的结果是创建的实例对象,只有手动返回对象类型值,才以用户自己返回的为准
所以返回的result实例对象的私有属性或方法,和函数上下文中的私有变量不存在直接关系。
这样new也行成了闭包,因为返回了实例对象,被外部变量所引用
所以类在JavaScript中实际上是function类型,本质是函数(构造函数),实例对象是Object类型
instanceof
instanceof:检测某个实例是否属于这个类
console.log(result instanceof Fn); //->true
区分字面量和构造函数创建的值
创建值有两种办法:
- 字面量方案
- 构造函数方案
let obj1 = {};
let obj2 = new Object();
对于对象来说两种方案没有区别,但对于原始值来说:
let n1 = 10; //Number类的一个实例「原始值」
let n2 = new Number(10); //Number类的一个实例「对象」
一个是原始值类型,一个是对象类型
console.log(n2.toFixed(2)); //->10.00
console.log(n1.toFixed(2)); //->10.00
原始值类型没有属性和方法,那么是如何做成员访问的?
js在内部有自己的处理机制:装箱和拆箱
装箱:
10->Object(10) 将原始值类型转化为「对象实例」,然后再做成员访问
拆箱:
console.log(n1 + 10); //->20
console.log(n2 + 10); //->20
n2对象会依次调用Symbol.toPrimitive/valueOf(),n2的valueOf()的返回原始值10,这个过程就是"拆箱"
instanceof不能识别原始值
注意:instanceof它的局限性:不能识别原始值
console.log(n2 instanceof Number); //->true
console.log(n1 instanceof Number); //->false
注意:
每个实例对象都是独立的内存地址:
let result1 = new Fn(10, 20);
let result2 = new Fn(30, 40);
console.log(result1 === result2); //->false
console.log(result1.say === result2.say); //->false
hasOwnProperty和[attr] in obj
Object为每一个对象提供hasOwnProperty方法,obj.hasOwnProperty([attr])检测attr是否是obj对象的私有属性[attr] in obj:检测attr是否为obj的一个属性「不论私有还是公有」
console.log(result1.hasOwnProperty('say')); //->true
console.log(result1.hasOwnProperty('hasOwnProperty')); //->false result1可以调用hasOwnProperty,说明hasOwnProperty是result1的一个成员「属性」,共有
console.log('say' in result1); //->true
console.log('hasOwnProperty' in result1); //->true
面试题:自己编写一个方法
hasPubProperty检测某个属性是否为对象的公有属性
function hasPubProperty(obj, attr) {
// 思路:基于in检测结果是TRUE「是它的一个属性」 & 基于hasOwnProperty检测结果是false「不是私有的」,那么一定是公有的属性
return (attr in obj) && !obj.hasOwnProperty(attr);
}
console.log(hasPubProperty(result1, 'hasOwnProperty')); //->true
console.log(hasPubProperty(result1, 'say')); //->false
这个方法的弊端:只能是这种情况“某个属性不是私有的而是公有的”,但是如果这个属性 私有中也有且公有中也有 ,基于这个方法结果是false,但是这个属性确实是他的公有属性
for in 循环
for in循环可以用来迭代对象
let arr = [10, 20, 30];
for (let i = 0; i < arr.length; i++) {
console.log(arr[i], i);
}
for (let key in arr) {
console.log(arr[key], key);//注意key为字符串形式
}
for循环本质不是遍历数组,是自己控制一个数字索引的循环逻辑,例如i=0 i<3 i++,自己控制循环数字三次, 每一轮循环i的值,恰好是我们想获取当前数组中这一项的索引「i从零开始,数组索引也是从零开始」,所以再基于成员访问获取即可。即for是循环索引,然后按照索引拿值
for in本质是迭代对象,按照本身的结构(键值对)去一一迭代的
所以for循环比for in循环性能稍微好一点
for in内置的缺陷
- 不能迭代
Symbol属性 - 迭代的时候不一定按照自己编写的键值对顺序迭代「优先迭代数字属性{小->大},再去迭代非数字属性{按自己编写顺序}」
- 不仅会迭代对象的私有属性,对于一些自己扩展的公有属性也会迭代到「迭代可枚举的{一般自己设定的都是可枚举的,内置的是不可枚举的}」
Object.prototype.sum = function sum() {};
let obj = {
name: 'xxx',
age: 12,
0: 100,
1: 200,
teacher: 'mtt',
[Symbol('AA')]: 300
};
for (let key in obj) {
console.log(key); //'0' '1' 'name' 'age' 'teacher' 'sum'
}
上面的顺序是数字属性,非数字属性,公有属性
如果不想迭代公有的,即使内部机制找到了,我们也不让其做任何的处理,这样做即可:
for (let key in obj) {
// 先找所有私有,一但发现这个是公有的,说明私有的都找完了,停止循环「注意不含Symbol」
if (!obj.hasOwnProperty(key)) break;
console.log(key);
}
Object.getOwnPropertySymbols(obj)
Object.getOwnPropertySymbols(obj)获取对象所有私有的Symbol属性「数组」
Object.keys(obj) 和Object.getOwnPropertyNames(obj)
Object.keys(obj) 和Object.getOwnPropertyNames(obj)获取对象所有非Symbol的私有属性「数组」
两个合在一起就能拿到所有的私有属性
题目:写一个函数,遍历所有的私有成员,包括
symbol属性的成员:
function each(obj, callback) {
let keys = Object.keys(obj),
key = null,
value = null,
i = 0,
len = 0;
if (typeof Symbol !== "undefined") {
// 支持Symbol
keys = keys.concat(Object.getOwnPropertySymbols(obj));
}
len = keys.length;
if (typeof callback !== "function") callback = function () {};
for (; i < len; i++) {
key = keys[i];
value = obj[key];
callback(value, key);
}
}
each(obj, (value, key) => {
console.log(value, key);
});
注意,对象属性的类型只可能是String或者Symbol
obj[1]等同于obj['1']
每一个Symbol类型的值都是独一无二的
会把属性自动转化成String类型
原型与原型链的必要的基础知识
protptype
JavaScript 规定,每个函数都有一个
prototype属性,指向一个对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型。
原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。
function Animal(name) {
this.name = name;
}
Animal.prototype.color = 'white';
Animal.prototype.meow = function () {
console.log('喵喵');
};
var cat1 = new Animal('大毛');
var cat2 = new Animal('二毛');
cat1.color // 'white'
cat2.color // 'white'
cat1.meow === cat2.meow // true
优点:共享属性或者方法,节省内存
function Cat(name, color) {
this.name = name;
this.color = color;
this.meow = function () {
console.log('喵喵');
};
}
如果像上面这样,每新建一个实例,就会新建一个同样行为的meow方法,浪费系统资源
constructor 属性
prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数。
Object.prototype.__proto__
实例对象的__proto__属性,返回该对象的原型对象,即构造函数的prototype属性。
var obj = new Object();
obj.__proto__ === Object.prototype
// true
obj.__proto__ === obj.constructor.prototype
// true
__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性。
原型链
JavaScript 规定,所有对象都有自己的原型对象(
prototype)。
- 一方面,任何一个对象,都可以充当其他对象的原型
- 另一方面,由于原型对象也是对象,所以它也有自己的原型。
因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型对象的原型……
如果一层层地上溯,所有对象的原型最终都可以上溯到
Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOf和toString方法的原因,因为这是从Object.prototype继承的。
Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null。
Object.getPrototypeOf(Object.prototype)//null
Object.getPrototypeOf方法返回参数对象的原型
读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的
Object.prototype还是找不到,则返回undefined。
如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性。
原型与原型链基础知识总结
-
每一个类(函数)都具备
prototype,并且值是一个对象。这个原型对象的上所有属性和方法,都能被实例对象共享 -
原型对象上具备一个属性
constructor,指向类本身 -
每一个对象(普通对象、prototype、实例、函数等)都具备
__proto__,值是当前实例所属类的原型对象
以上是关于原型链的基本知识,接下来的文章会详细说明原型链的机制