JavaScript面向对象基础总结
面向对象和类的定义
类:所谓“类”就是对象的模板,对象就是“类”的实例。对象通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成
那么,“对象”(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
和 in obj
Object
为每一个对象提供hasOwnProperty
方法,obj.hasOwnProperty()
检测attr
是否是obj
对象的私有属性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
类型