JavaScript 面向对象

663 阅读14分钟

面向对象

封装, 继承, 多态

属性和方法的分类

辨析:

  1. 通过构造函数创建的对象, 我们称之为"实例对象"
  2. 构造函数也是一个对象, 所以我们也可以给构造函数动态添加属性和方法

在 JavaScript 中属性和方法分类两类

  1. 实例属性/实例方法
    • 在企业开发中通过实例对象访问的属性, 我们就称之为实例属性
    • 在企业开发中通过实例对象调用的方法, 我们就称之为实例方法
  2. 静态属性/静态方法 (这倒是和 Java 有点像了)
    • 在企业开发中通过构造函数访问的属性, 我们就称之为静态属性
    • 在企业开发中通过构造函数调用的方法, 我们就称之为静态方法

封装

  1. 局部变量和局部函数
    • 无论是ES6之前还是ES6, 只要定义一个函数就会开启一个新的作用域
    • 只要在这个新的作用域中, 通过let/var定义的变量就是局部变量
    • 只要在这个新的作用域中, 定义的函数就是局部函数
  2. 什么是对象的私有变量和函数
    • 默认情况下对象中的属性和方法都是公有的, 只要拿到对象就能操作对象的属性和方法
    • 外界不能直接访问的变量和函数就是私有变量和是有函数
    • 构造函数的本质也是一个函数, 所以也会开启一个新的作用域, 所以在构造函数中定义的变量和函数就是私有和函数 这里的怪异之处在于: this.xxx 居然不算是函数中定义的变量 哦哦哦,私有属性的本质就是一个局部变量, 并不是真正的属性, 所以如果通过 对象.xxx的方式是找不到私有属性的, 所以会给当前对象新增一个不存在的属性
  3. 什么是封装?
    • 封装性就是隐藏实现细节,仅对外公开接口
  4. 为什么要封装?
    • 不封装的缺点:当一个类把自己的成员变量暴露给外部的时候,那么该类就失去对属性的管理权,别人可以任意的修改你的属性
    • 封装就是将数据隐藏起来,只能用此类的方法才可以读取或者设置数据,不可被外部任意修改. 封装是面向对象设计本质(将变化隔离)。这样降低了数据被误用的可能 (提高安全性和灵活性)

继承 (本质只是复用, 伪继承!)

方法一
  • 修改子类构造函数原型对象的指向
function Person() {
    this.name = null;
    this.age = 0;
    this.say = function () {
        console.log(this.name, this.age);
    }
}

function Student() {
    this.score = 0;
    this.study = function () {
        console.log("day day up");
    }
}
Student.prototype = new Person(); // 将 Person 的实例对象作为构造函数 Student 的原型对象
Student.prototype.constructor = Student; // 显式地将 Student 的原型对象的 constructor 指回 Student

let stu = new Student();
stu.name = "zs"; // 实例对象默认拥有原型对象的属性, 好鬼酷!
stu.age = 18;
stu.score = 99;
stu.say();
stu.study();
  • 方法一的弊端: 如果父类(妈的不先学 Java 知道个 p 的父类的概念)是个带参构造函数, 子构造函数并不能用通过带参赋值给继承自父类的属性赋值
function Person(myName, myAge) {
    this.name = myName;
    this.age = myAge;
    this.say = function () {
        console.log(this.name, this.age);
    }
}

// 学生 is a 人 , 学生是一个人
function Student(myName, myAge, myScore) { // 这样不报错但是不得行
    this.score = myScore;
    this.study = function () {
        console.log("day day up");
    }
}
方法二
  • 在子类构造函数中改变父类构造函数的 this 指向 从而解决子类构造函数无法在创建时通过传参指定继承自父类的属性的属性值 弊端: 这样搞 Student 用不了 Person 的原型对象的属性和方法

     function Person(myName, myAge) {
         // let per = new Object();   // 系统自动添加的
         // let this = per;  // 系统自动添加的
         // this = stu;  // 子类构造函数改了父类构造函数的 this
         this.name = myName; // stu.name = myName;
         this.age = myAge; // stu.age = myAge;
         this.say = function () { // stu.say = function () {}
             console.log(this.name, this.age);
         }
         // return this;  // 系统自动添加的
     }
    function Student(myName, myAge, myScore) { // 我他妈服
        // let stu = new Object();  // 系统自动添加的
        // let this = stu;  // 系统自动添加的
        Person.call(this, myName, myAge); //  Person.call(stu);
        this.score = myScore;
        this.study = function () {
            console.log("day day up");
        }
        // return this;  // 系统自动添加的
    }
    let stu = new Student("ww", 19, 99);
    // stu.name = "zs";
    // stu.age = 18;
    // stu.score = 99;
    console.log(stu.score);
    stu.say();
    stu.study();
    
  • bind, call, apply (作用是复用!)

  1. this是什么? 谁调用当前函数或者方法, this 就是谁
  2. 这三个方法的作用是什么? 这三个方法都是用于修改函数或者方法中的 this 的
    1. bind 方法作用 let boundFunc = func.bind(thisArg[, arg1[, arg2[, ...argN]]]) 修改函数或者方法中的 this 为指定的对象, 并且会返回一个修改之后的新函数给我们 注意点: bind 方法除了可以修改 this 以外, 还可以传递参数, 只不过参数必须写在 this 对象的后面
    2. call 方法作用 func.call([thisArg[, arg1, arg2, ...argN]]) 修改函数或者方法中的 this 为指定的对象, 并且会立即调用修改之后的函数 注意点: call 方法除了可以修改 this 以外, 还可以传递参数, 只不过参数必须写在 this 对象的后面
    3. apply 方法作用 func.apply(thisArg, [ argsArray]) 修改函数或者方法中的 this 为指定的对象, 并且会立即调用修改之后的函数 注意点: apply 方法除了可以修改 this 以外, 还可以传递参数, 只不过参数必须通过数组的方式传递
  • 注意到: 本质上 Student 和 Person 半毛钱关系都没有, Student 只是借用了 Person 的属性和方法, 这种约定俗成的继承和 Java 的规矩上的继承完全不是一回事儿
方法三
  • 在方法二的基础上通过更改 Student 原型对象的指向 使得 Student 产的对象不仅能使用 Person 的属性和方法 还能使用 Person 原型对象的属性和方法 弊端:

    • 改变了 Person 的原型对象的 constructor 的指向, 破坏了 Person 的原型关系
    • 用了这法子以后给 Student.prototype 添加属性方法的时候会污染 Person 的原型对象
    function Person(myName, myAge) {
        this.name = myName;
        this.age = myAge;
    }
    Person.prototype.say = function () {
        console.log(this.name, this.age);
    }
    function Student(myName, myAge, myScore) {
        Person.call(this, myName, myAge);
        this.score = myScore;
        this.study = function () {
            console.log("day day up");
        }
    }
    /*
    注意点:
    要想使用Person原型对象中的属性和方法,
    那么就必须将Student的原型对象改为Person的原型对象才可以
    */
    Student.prototype = Person.prototype; // 这个是败笔
    Student.prototype.constructor = Student;
    
    let stu = new Student("ww", 19, 99);
    console.log(stu.score);
    stu.say();
    stu.study();
    
方法四 (只用这个就好)

js 中继承的终极方法

  1. 在子类的构造函数中通过 call 借助父类的构造函数
  2. 将子类的原型对象修改为父类的实例对象 优点: 使得 Student 产的对象不仅能使用 Person 的属性和方法 还能使用 Person 原型对象的属性和方法 最重要的是不会污染 Person 的原型
function Person(myName, myAge) {
    // let per = new Object();
    // let this = per;
    // this = stu;
    this.name = myName; // stu.name = myName;
    this.age = myAge; // stu.age = myAge;
    // return this;
}
Person.prototype.say = function () {
    console.log(this.name, this.age);
}
function Student(myName, myAge, myScore) {
    Person.call(this, myName, myAge);
    this.score = myScore;
    this.study = function () {
        console.log("day day up");
    }
}

Student.prototype = new Person(); // 小改动, 大不同
Student.prototype.constructor = Student;
Student.prototype.run = function(){
    console.log("run");
}

let per = new Person();
per.run();

多态

强类型语言与弱类型语言
  1. 什么是强类型语言? 一般编译型语言都是强类型语言, 强类型语言,要求变量的使用要严格符合定义 例如定义 int num; 那么 num 中将来就能够存储整型数据
  2. 什么是弱类型语言? 一般解释型语言都是弱类型语言, 弱类型语言, 不会要求变量的使用要严格符合定义 例如定义 let num; num 中可以存储整型, 可以存储布尔类型等
  3. 由于js语言是弱类型的语言, 所以我们不用关注多态
多态

多态是指事物的多种状态, js 天生是多态的

例如: 按下 F1 键这个动作, 如果当前在 webstorm 界面下弹出的就是 webstorm 的帮助文档; 如果当前在 Word 下弹出的就是 Word 帮助; 同一个事件发生在不同的对象上会产生不同的结果 (不同类的对象调同名方法有不同的行为)。

多态在编程语言中的体现 父类型变量保存子类型对象, 父类型变量当前保存的对象不同, 产生的结果也不同

function Dog() {
    this.eat = function () {
        console.log(" 狗吃东西");
    }
}

function Cat() {
    this.eat = function () {
        console.log(" 猫吃东西");
    }
}

function feed(animal){
    animal.eat();
}

let dog = new Dog();
feed(dog);

let cat = new Cat();
feed(cat);

小结

私有属性本质不是属性, 继承本质只是借用, 多态是天生的, js 净整些花活儿

从法三到法四的进化来看, 发现做同一件事, 用到的权限和资源越少反而越好, 挺合理

ES6 的类

说真的, 不先学 Java 就来看这个能学明白的就应该去搞算法

class 关键字

  • 在 ES6 之前通过构造函数来定义一个类
function Person(myName, myAge) {
    // 实例属性
    this.name = myName;
    this.age = myAge;

    // 实例方法
    this.say = function () {
        console.log(this.name, this.age);
    }
    // 静态属性
    Person.num = 666;
    // 静态方法
    Person.run = function () {
        console.log("run");
    }
}

let p = new Person("zs", 18);
p.say();
console.log(Person.num);
Person.run();
  • 从 ES6 开始系统提供了一个名称叫做class的关键字, 这个关键字就是专门用于定义类的 class 不纯是构造函数套壳子, class 的 this 和 构造函数的 this 貌似还有不同
class Person{
    // 当我们通过 new 创建对象的时候, 系统会自动调用 constructor
    // constructor 我们也称之为构造函数
    // ES6 标准规定通过 class 定义的实例属性都要在 constructor 中添加
    constructor(myName, myAge){
        this.name = myName;
        this.age = myAge;
        // 一般定义原型方法才恰当嗷
        this.hi = function () {
            console.log("hi");
        }
    }
    // 原型方法, 默认存到原型对象中
    say(){
        console.log(this.name, this.age);
    }
    // 静态属性
    static num = 666;
    // 静态方法
    static run() {
        console.log("run");
    }
}
// let p = new Person();
let p = new Person("zs", 18);
p.say();
console.log(Person.num);
Person.run();

注意点:

  • 如果通过class定义类, 则不能自定义这个类的原型对象
  • 如果想将属性和方法保存到原型中, 只能动态给原型对象添加属性和方法 (优化构造函数三版优化里有说明什么叫动态添加)
  • ES6 标准规定通过 class 定义的实例属性都只能在 constructor 中添加, 直接在 class 里加的属性的方式只有 chrome 才认

extends 关键字

ES6 之前的继承
  1. 在子类中通过 call/ apply 方法借助父类的构造函数
  2. 将子类的原型对象设置为父类的实例对象
function Person(myName, myAge) {
    this.name = myName;
    this.age = myAge;
}
Person.prototype.say =  function () {
    console.log(this.name, this.age);
}
function Student(myName, myAge, myScore) {
    // 1.在子类中通过 call/apply 方法借助父类的构造函数
    Person.call(this, myName, myAge);
    this.score = myScore;
    this.study = function () {
        console.log("day day up");
    }
}
// 2. 将子类的原型对象设置为父类的实例对象
Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student("zs", 18, 99);
stu.say();
ES6 之后的继承
  1. 在子类后面添加extends并指定父类的名称
  2. 在子类的constructor构造函数中通过super方法借助父类的构造函数
 class Person{
     constructor(myName, myAge){
         this.name = myName;
         this.age = myAge;
     }
     say(){
         console.log(this.name, this.age);
     }
 }

// 1. 在子类后面添加extends并指定父类的名称
class Student extends Person{
    constructor(myName, myAge, myScore){
        // 2. 在子类的constructor构造函数中通过super方法借助父类的构造函数
        super(myName, myAge);
        this.score = myScore;
    }
    study(){
        console.log("day day up");
    }
}
let stu = new Student("zs", 18, 98);
stu.say();

类, 属性, 原型

obj.constructor.name

每个实例对象的 __proto__ 都指向其构造函数的原型对象, 实例对象本身没有 constructor 属性但其构造函数的原型对象有, 于是 console.log(实例对象.constructor) 可以得到该实例对象的构造函数, 构造函数有个默认属性 name, 其值即为该构造函数的名字, 也就是 JS 中的类名. 所以, 用实例对象.constructor.name可以拿到类名

function Person() {
    this.name = "lnj";
    this.age = 34;
    this.say = function () {
        console.log(this.name, this.age);
    }
}

let p = new Person();
console.log(typeof p); // object
console.log(p.constructor.name); // Person
instanceof 关键字

instanceof 关键字的作用: instanceof 用于判断 "对象" 是否是指定构造函数的 "实例"

class Person{
    name = "fhs";
}
let p = new Person();
console.log(p instanceof Person); // true

注意点: 只要构造函数的原型对象出现在实例对象的原型链中都会返回 true

function Person(myName) {
    this.name = myName;
}
function Student(myName, myScore) {
    Person.call(this, myName);
    this.score = myScore;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student();
console.log(stu instanceof Student); // true
console.log(stu instanceof Person); // true
isPrototypeOf 方法

什么是isPrototypeOf属性? isPrototypeOf 是原型对象的一个方法, 用于判断一个对象是否是另一个对象的原型

class Person{
    name = "fhs";
}
let  p = new Person();
console.log(Person.prototype.isPrototypeOf(p)); // true

class Cat{
    name = "mm";
}
console.log(Cat.prototype.isPrototypeOf(p)); // false

注意点: 只要调用者在传入对象的原型链上都会返回 true

function Person(myName) {
    this.name = myName;
}
function Student(myName, myScore) {
    Person.call(this, myName);
    this.score = myScore;
}
Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student();
console.log(Person.prototype.isPrototypeOf(stu)); // true
in 关键字

in 关键字的作用: 判断某一个对象是否拥有某一个属性

注意点: 只要类中或者原型对象中有, 就会返回 true

class Person{
    name = null;
	age = 0;
}
Person.prototype.height = 0;

let p = new Person();
console.log("name" in p); // true
console.log("width" in p); // false
console.log("height" in p); // true
hasOwnProperty 方法

hasOwnProperty 方法的作用: 判断某一个对象自身是否拥有某一个属性

注意点: 只会去类中查找有没有, 不会去原型对象中查找

class Person{
    name = null;
age = 0;
}
Person.prototype.height = 0;

let p = new Person();
console.log(p.hasOwnProperty("name")); // true
console.log(p.hasOwnProperty("height")); // false

属性, 方法的增删改查

方括号引号的作用和 . 作用相同. 但方括号不带引号时能做 . 做不到的事, 详见遍历对象

  • 通过 .
class Person{}

let p = new Person();        
p.name = "fhs";
p.say = function(){
    console.log("hello world");
}
  • 通过方括号引号
class Person{}

let p = new Person();        
p["name"] = "zs";
p["say"] = function(){
	console.log("hello world");
}

. 和方括号引号 效果是一样的, 以下 . 操作都可用方括号引号代替

删 (delete 关键字)

  • 用 delete 关键字来删除
class Person{
    name = 'fhs';
	age = 23;
    say(){
        console.log(this.name, this.age);
    }
}

let p = new Person();
delete p.name;
delete p.say;
console.log(p);

  • 直接给属性赋新值即可
class Person{
    name = 'fhs';
	age = 23;
	say(){
        console.log("hello");
    }
}

let p = new Person();
p.name = "xhx";
p.say = function(){
	console.log("hi");
}
console.log(p);

  • 对象.属性 或 对象.方法 就能查了
class Person{
    name = 'fhs';
	age = 23;
	say(){
        console.log("hello");
    }
}

let p = new Person();
console.log(p.name);
console.log(p.say);

遍历对象

  1. 在JavaScript中对象和数组一样是可以遍历的
  2. 什么是对象的遍历? 对象的遍历就是依次取出对象中所有的属性和方法
  3. 如何遍历一个对象? 在JS中可以通过高级for循环来遍历对象
  4. 形式: for(let key in obj) {} 注意点: 只能历到对象本身的属性方法, 原型链上的不会被遍历到
  • 遍历不到原型链上的属性, 方法
class Person{
    constructor(myName, myAge){
        this.name = myName;
        this.age = myAge;
    }
    // 注意点: ES6定义类的格式, 默认将方法放到原型对象中
    say(){
        console.log(this.name, this.age);
    }
}

let p = new Person("fhs", 34);
console.log(p);
for(let key in p){
    if(p[key] instanceof Function){
        continue;
    }
    console.log(p[key]); // name age
    // 注意到这里没有 say, 因为 say 在原型对象上
}
  • 在类里面这点要注意
function Person(myName, myAge){
    this.name = myName;
    this.age = myAge;
    this.say = function(){
        console.log(this.name, this.age);
    }
}

let p = new Person("fhs", 34);
console.log(p); // 这时 say 才在对象里
for(let key in p){
    console.log(key); // name / age / say
    // 以下代码的含义取出 p 对象中名称叫做当前遍历到的名称的属性或者方法的取值
    console.log(p[key]); // p["name"] / p["age"] / p["say"]
    // 以下代码的含义取出 p 对象中名称叫做 key 的属性的取值 (那当然是没有, 所以这样不行)
    console.log(p.key); // undefined
}
  • 还可以通过判断使不遍历方法
function Person(myName, myAge){
    this.name = myName;
    this.age = myAge;
    this.say = function(){
        console.log(this.name, this.age);
    }
}

let p = new Person("fhs", 34);
console.log(p); // 这时 say 才在对象里
    for(let key in p){
        if(p[key] instanceof Function){ // 加这个判断以去掉方法, 一般对象原型链上没有 Function 嗷
            continue;
    }
	console.log(key); // name / age
}

对象解构赋值

对象与数组解构赋值的异同

  • 对象的解构赋值和数组的解构赋值符号不一样

    • 数组解构使用 [], 对象解构使用 {}
  • 另外的不同:

    • 在对象解构赋值中, 左边的变量名称必须和对象的属性名称一致, 才能解构出数据
    • 且值与名字对应, 而非与顺序对应
  • 其它的一模一样

let obj = {
    name: "lnj",
    age: 34
}

// 法一, 完全没用到解构赋值
// let name = obj.name;
// let age = obj.age;

// 法二
// let {name, age} = obj;

// 法三
let {name, age} = {name: "lnj",age: 34};
console.log(name, age);

解构赋值应用场景

不管是数组解构赋值还是对象结构赋值, 都可以用在函数传参中 倒不是有什么用, 主要是够花哨

数组解构赋值的应用
let arr = [1, 3];

// function sum(a, b) {
function sum([a, b]) {
    return a + b;
}

// let res = sum(arr[0], arr[1]);
let res = sum(arr);
console.log(res);
对象解构赋值的应用
let obj = {
    name: "fhs",
    age: 23
}

// function say(name, age) {
function say({name, age}) {
    console.log(name, age);
}

// say(obj.name, obj.age);
say(obj);
可以在解构赋值时给变量==换名字==
let user = { 
    name: "John", 
    years: 30 
};

// let { name, years: age, isAdmin = false } = user;
let { name, years: age, isAdmin = false } = user;
console.log(name); // John
console.log(age); // 30
console.log(isAdmin); // false