JavaScript面向对象系列之基础概念总结

317 阅读9分钟

面向对象和类的定义

:所谓“类”就是对象的模板,对象就是“类”的实例。对象通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成

那么,“对象”(object)到底是什么?我们从两个层次来理解。 (1)对象是单个实物的抽象。 一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个远程服务器连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程。 (2)对象是一个容器,封装了属性(property)和方法(method)。 属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是哪一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等)。

JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)原型链(prototype)

以上概念来自阮一峰JavaScript教程-面向对象编程

开发示例:如果一个页面有两个轮播图,我想让这两个轮播图各自单独运行,但是核心的代码逻辑不变,且两个实例互不影响,他们的核心运行逻辑和表现都一样,但是需要各自单独运行他们自己的逻辑,且有自己的数据和状态,这时候就要用到面向对象的开发方法。插件封装组件封装,都是面向对象的思想的应用,都是对私有化和公有化的合理管控。

JS本身就是面向对象的编程语言,而他本身也是基于面向对象的思想构建出来的语言。

image。png

面向对象思想:

  • 对象:万物皆对象
  • 类:对“对象”的归类和细分
  • 实例:以类为模板创造出来的具体成员

内置类

JS中的类:内置类、自定义类

内置类:

  • 每一种数据类型有自己对应的内置类:NumberStringBooleanSymbolBigIntObjectArrayRegExpDateSetMapArrayBuffer...)、Function
  • 我们平时接触的具体值就是对应的类的实例,例如:10就是Number类的一个实例,[10,20] -> Array -> Object ...
  • 每一个HTML元素标签「包含window/document等」在JS中都有自己的内置类,例如:div -> HTMLDivElement -> HTMLElement -> Element -> Node -> EventTarget -> Object

image。png 一个DOM元素对象

image。png

在封装,抽象的时候,要始终记得使用面向对象的思想

自己创造类

普通函数执行

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要根据函数运行时所在的环境决定

image。png

构造函数执行

let result1 = new Fn(10, 20);
let result2 = new Fn(30, 40);
console.log(result1.say === result2.say); //->false

使用new代表把函数当作构造函数执行,这个构造函数就是自定义的类

  • Fn被称为或者构造函数
  • result被称为当前类的一个实例

image。png

使用new关键字执行构造函数比普通函数执行做了如下操作:

  1. 在初始化this之前,默认创建一个空对象(作为返回的实例对象),在初始化this的时候,将this指向创建的实例对象
  2. 所以在代码执行阶段,函数体中遇到this.xxx = xxx的赋值操作,都是给实例对象设置私有的属性或方法(上下文中的私有变量和实例对象没有直接关系,只有遇到类似this.xxx = xxx的赋值,才有关系)
  3. 如果函数不设置返回值或者返回的是原始值类型,则默认返回的结果是创建的实例对象,只有手动返回对象类型值,才以用户自己返回的为准

image。png

所以返回的result实例对象的私有属性或方法,和函数上下文中的私有变量不存在直接关系。

这样new也行成了闭包,因为返回了实例对象,被外部变量所引用

所以类在JavaScript中实际上是function类型,本质是函数(构造函数),实例对象是Object类型 image。png

instanceof

instanceof:检测某个实例是否属于这个类

console.log(result instanceof Fn); //->true

区分字面量和构造函数创建的值

创建值有两种办法:

  • 字面量方案
  • 构造函数方案
 let obj1 = {};
 let obj2 = new Object();

对于对象来说两种方案没有区别,但对于原始值来说:

 let n1 = 10; //Number类的一个实例「原始值」
 let n2 = new Number(10); //Number类的一个实例「对象」

一个是原始值类型,一个是对象类型 image。png

console.log(n2.toFixed(2)); //->10.00
console.log(n1.toFixed(2)); //->10.00 

原始值类型没有属性和方法,那么是如何做成员访问的?

js在内部有自己的处理机制:装箱和拆箱

装箱:

10->Object(10) 将原始值类型转化为「对象实例」,然后再做成员访问

image。png

拆箱:

 console.log(n1 + 10); //->20
 console.log(n2 + 10); //->20 

n2对象会依次调用Symbol.toPrimitive/valueOf()n2valueOf()的返回原始值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的私有属性「数组」

两个合在一起就能拿到所有的私有属性 image。png

题目:写一个函数,遍历所有的私有成员,包括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);
});

image。png

注意,对象属性的类型只可能是String或者Symbol

obj[1]等同于obj['1']

image.png

每一个Symbol类型的值都是独一无二的

image.png

会把属性自动转化成String类型 image。png

原型与原型链的必要的基础知识

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

image。png

优点:共享属性或者方法,节省内存

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的属性。这就是所有对象都有valueOftoString方法的原因,因为这是从Object.prototype继承的。

Object.prototype的原型是nullnull没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null

Object.getPrototypeOf(Object.prototype)//null 

Object.getPrototypeOf方法返回参数对象的原型

读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined

如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性。

原型与原型链基础知识总结

  • 每一个类(函数)都具备prototype,并且值是一个对象。这个原型对象的上所有属性和方法,都能被实例对象共享

  • 原型对象上具备一个属性constructor,指向类本身

  • 每一个对象(普通对象、prototype、实例、函数等)都具备__proto__,值是当前实例所属类的原型对象

以上是关于原型链的基本知识,接下来的文章会详细说明原型链的机制