JavaScript 笔记(七):面向对象

JavaScript 笔记(七):面向对象

默认对象

在 JavaScript 中,系统提供了一个默认的类 Object,我们可以通过此类来创建对象,可以通过某些方式为此对象添加属性和方法,示例如下:

let obj = new Object();

obj.name = "Reyn Morales";
obj.age = 21;
obj.say = () => console.log("Hello, World!");

console.log(obj.name);  // Reyn Morales
console.log(obj.age);   // 21
obj.say();  // Hello, World!

除了可以使用 new 运算符之外,还可以使用 {} 创建一个对象,示例如下:

let obj = {};

obj.name = "Reyn Morales";
obj.age = 21;
obj.say = () => console.log("Hello, World!");

console.log(obj.name);  // Reyn Morales
console.log(obj.age);   // 21
obj.say();  // Hello, World!

此外,还可以在定义的时候初始化,而不是在定义之后再初始化,示例如下:

let obj = {
    name: "Reyn Morales",
    age: 21,
    say: function() {
        console.log("Hello, World!");
    }
};

console.log(obj.name);  // Reyn Morales
console.log(obj.age);   // 21
obj.say();  // Hello, World!

在定义对象并初始化时,属性与值之间使用 :,而属性与属性之间使用 ,

在 JavaScript 中,函数不需要和某个对象显示绑定,而方法必须和某个对象显示绑定在一起,二者的区别在于,函数可以直接调用,而方法不能直接调用,必须通过某个对象调用,此外,不论是函数或方法,内部都存在一个 this 引用,引用了调用当前函数或方法的对象,调用函数的对象通常是 window,而调用方法的对象通常是自定义对象,示例如下:

function say() {
    console.log(this);
}
window.say();
let person = {
    name: "Reyn",
    say: function() {
        console.log(this);
    }
};
person.say();   // {"name":"Reyn"}

工厂函数

工厂函数是一类用于创建对象的函数,可以有效减少代码的冗余度,示例如下:

function createPerson(oName, oAge) {
    let obj = new Object();
    obj.name = oName;
    obj.age = oAge;
    obj.say = () => console.log("Hello World");
    return obj;
}

let person1 = createPerson("Reyn", 21);
let person2 = createPerson("Jane", 20);

console.log(person1);   // {"name":"Reyn","age":21}
console.log(person2);   // {"name":"Jane","age":20}

构造函数

构造函数类似于工厂函数,是一类用于创建对象的函数,二者的区别如下:

  1. 构造函数的函数名称首字母必须大写
  2. 构造函数必须使用 new 运算符调用

以下是一个示例:

let Person = function(oName, oAge) {
    this.name = oName;
    this.age = oAge;
    this.say = () => console.log("Hello World");      
}

let person1 = new Person("Reyn", 21);
let person2 = new Person("Jane", 20);

console.log(person1);   // {"name":"Reyn","age":21}
console.log(person2);   // {"name":"Jane","age":20}

值得注意的是,不能以箭头函数的形式创建构造函数,此外,当我们调用构造函数时,系统自动实现了如下过程:

  1. 在构造函数中定义一个 Object 对象 obj
  2. 在构造函数中定义一个 this 引用,并初始化为 obj
  3. 在构造函数中返回此 this 引用的对象

在构造函数中添加语句后,示例如下:

let Person = function(oName, oAge) {
    let obj = new Object(); // 在构造函数中定义一个 Object 对象 obj
    let this = obj; // 在构造函数中定义一个 this 引用,并初始化为 obj
    this.name = oName;
    this.age = oAge;
    this.say = () => console.log("Hello World");
    return this;    // 在构造函数中返回此 this 引用的对象
}

已知,如果使用恒等运算符(===)判断函数名称,表示判断两个函数是否在同一块内存空间中,即函数名称是否引用的是同一块内存空间,示例如下:

function say() {
    console.log("Hello World");
}
console.log(say == say);    // true

因此,我们可以通过此方法判断两个对象中的方法是否为同一个方法,示例如下:

let Person = function(oName, oAge) {
    this.name = oName;
    this.age = oAge;
    this.say = () => console.log("Hello World");      
}

let person1 = new Person("Reyn", 21);
let person2 = new Person("Jane", 20);

console.log(person1.say === person2.say);   // false

根据上述示例可知,不同对象中的方法是不相同的,更确切的说是不在同一块内存空间中,而对于每一个对象来说,say 方法显然是相同的,如果创建若干个不同的对象,将造成更多的浪费,因此,我们可以通过将函数存储在某个公共区域中,而使每一个对象中的方法变量都引用此方法即可,示例如下:

function fn() {
    console.log("Hello World");
}

let Person = function(oName, oAge) {
    this.name = oName;
    this.age = oAge;
    this.say = fn;      
}

let person1 = new Person("Reyn", 21);
let person2 = new Person("Jane", 20);

console.log(person1.say === person2.say);   // true

我们将全局变量中的函数赋值给构造函数中方法变量,从而实现使每一个对象中的方法变量都引用同一个方法,但是将使代码的可读性降低,因此,我们可以将公共函数存放到某个专门用于存放函数变量的对象当中,如下所示:

let fns = {
    say: function() {
        console.log("Hello World");
    }
}

let Person = function(oName, oAge) {
    this.name = oName;
    this.age = oAge;
    this.say = fns.say;      
}

let person1 = new Person("Reyn", 21);
let person2 = new Person("Jane", 20);

console.log(person1.say === person2.say);   // true

以上方法完美解决了之前一种方式存在的问题,此外,还有更加专业的写法,如下所示:

let Person = function(oName, oAge) {
    this.name = oName;
    this.age = oAge;  
}

Person.prototype = {
    say: function() {
        console.log("Hello World");
    }
}

let person1 = new Person("Reyn", 21);
let person2 = new Person("Jane", 20);

console.log(person1.say === person2.say);   // true

在以上示例中,我们将公共函数存放到了 Person 的 prototype 所引用的对象中,prototype 是 Person 构造函数中的一个默认属性,被 prototype 属性所引用的对象的作用类似于上一种方式中的 fns 对象,专门用于存放类的某些公共部分

prototype

prototype 是构造函数中的一个默认属性,它引用了一个对象,此对象可以存储一个类的某些公共属性或方法,如果在一个类的构造函数中实现了在 prototype 中已经存在的方法,那么在类中实现的方法将覆盖 prototype 中的方法,示例如下:

let Person = function(oName, oAge) {
    this.name = oName;
    this.age = oAge;  
    this.say = function() {
        console.log("Hello China");
    }
}

Person.prototype = {
    currentType: "Person",
    say: function() {
        console.log("Hello World");
    }
}

let person = new Person("Reyn", 21);
console.log(person.currentType);    // Person
console.log(person.say());  // Hello China

原型对象

  1. 在一个构造函数中,存在一个默认名称为 prototype 的属性,引用了一个对象,此对象被称为原型对象
  2. 在原型对象中,存在一个默认名称为 constructor 的属性,引用了引用当前原型对象的 prototype 属性所在的构造函数
  3. 在每一个实例对象中,存在一个默认名称为 proto 的属性,引用了此实例对象构造函数中的 prototype 属性引用的原型对象

通过构造函数创建的对象被称为实例对象

Function 构造函数

在 JavaScript 中,函数是一种引用类型,即对象类型,因此函数本质上也是使用某个构造函数实例化出来的,此构造函数即为 Function 构造函数,而构造函数本质上也是一种函数,因此,构造函数也是通过 Function 构造函数实例化出来的一个实例对象,因此构造函数也有一个 proto 属性,它引用了 Function 构造函数中 prototype 属性引用的原型对象,而此原型对象中也存在一个 constructor 属性引用 Function 构造函数

任何对象总有一个名为 proto 的属性引用构造此实例对象的构造函数中的 prototype 属性引用的原型对象

Object 构造函数

Object 构造函数也是一种函数对象,它的 proto 属性引用了 Function 构造函数的原型对象,它的 prototype 属性引用了 Object 构造函数的原型对象,而此原型对象的 proto 属性的值为 null,即 Object 构造函数的原型对象之上再无其它构造函数的原型对象,此外,Function 构造函数的 proto 属性引用 Function 的原型对象

总结

综上所述,我们可以得出以下结论:

  1. Function 函数是所有函数的祖先函数
  2. 所有构造函数都有一个 prototype 属性
  3. 所有原型对象都有一个 constructor 属性
  4. 所有函数都是对象
  5. 所有对象都有一个 proto 属性

JavaScript 对象关系

原型链

在使用构造函数的 prototype 属性时,在优化之前,构造函数的原型对象的 constructor 属性引用构造函数,在优化之后,构造函数的 prototype 属性引用了另一个对象,导致构造函数的原型对象的 constructor 属性不再引用相应的构造函数,从而破坏了对象关系,示例如下:

function Person(oName, oAge) {
    this.name = oName;
    this.age = oAge;
}

console.log(Person.prototype.constructor);  // function Person

Person.prototype = {
    currentType: "Person",
    say: function() {
        console.log("Hello World");
    }
}

console.log(Person.prototype.constructor);  // function Object

在一般情况下,为了不破坏对象关系,可以通过在 Person.prototype 中添加 constructor 属性引用相应的构造函数即可,示例如下:

function Person(oName, oAge) {
    this.name = oName;
    this.age = oAge;
}

console.log(Person.prototype.constructor);  // function Person

Person.prototype = {
    constructor: Person,
    currentType: "Person",
    say: function() {
        console.log("Hello World");
    }
}

console.log(Person.prototype.constructor);  // function Person

在每一个对象中都存在一个 proto 属性,此属性引用了此对象的构造函数的原型对象,可以将此属性所引用的每一个原型对象视作链,此链被称为原型链,在使用某个对象中的属性或方法时,将使用此原型链,具体过程是,系统将现在此对象的构造函数中查找是否存在此属性或方法,如果存在,即使用构造函数中相应的属性或方法,如果不存在,那么就以原型链的方向向上查找在其构造函数的原型对象中是否存在此属性或方法,如果存在,即使用找到的属性或方法,如果不存在,那么就以原型链的方向继续向上查找,依次类推,直至找到 Object 构造函数的原型对象,如果 Object 原型对象中任然没有待查找的属性或方法,那么将会报错

此外,在使用构造函数创建一个对象之后,如果通过此对象设置一个不存在的值时,将不会查找并设置原型对象中可能存在的值,而是为实例对象新添加一个属性,示例如下:

function Person(oName, oAge) {
    this.name = oName;
    this.age = oAge;
}

Person.prototype = {
    constructor: Person,
    currentType: "Person",
    say: function() {
        console.log("Hello World");
    }
}

let person1 = new Person("Reyn", 21);

console.log(person1.currentType);   // Person
console.log(person1.__proto__.currentType); // Person

person1.currentType = "SuperMan";

console.log(person1.currentType);   // SuperMan
console.log(person1.__proto__.currentType); // Person

let person2 = new Person("Jane", 20);

console.log(person2.currentType);   // Person
console.log(person2.__proto__.currentType); // Person

特性

封装

封装性即隐藏实现细节,仅对外界公开接口,在 JavaScript 中,封装性体现在构造函数中,由于在函数体中的作用域为局部作用域,不论使用 var 还是 let 定义的变量均为局部变量,而在其中的函数均为局部函数,此时外界无法访问相应的变量或函数,在构造函数中使用 var 或 let 定义的变量以及函数被称为私有属性和方法,使用 this 开始的变量和函数被称为公有属性和方法,示例如下:

function Person(oName, oAge) {
    this.name = oName;
    this.say = () => console.log("Hello World");

    let age = oAge;

    this.setAge = (oAge) => {
        if (oAge >= 0) {
            age = oAge;
        }
    };

    this.getAge = function() {
        return age;
    }
}

let person = new Person("Reyn", 21);

console.log(person.name);   // Reyn
console.log(person.age);    // undefined

console.log(person.getAge());   // 21

person.age = 18;
console.log(person.age);    // 18
console.log(person.getAge());   // 21

此示例程序的倒数第三行将为此对象添加一个公有属性 age 并将其值设置为 18,而不是修改构造函数中的局部变量或在原型链中查找原型对象的公有属性

继承

继承性即使一个类获得另一个类的所有属性或方法,获得另一个类属性或方法的类被称为子类,被另一个类获得属性或方法的类被称为父类,通常情况下,如果两个类满足 is a 的关系时,既可以使用继承来优化代码,减少代码的冗余度,示例如下:

function Person() {
    this.name = null;
    this.age = 0;
    this.say = () => console.log("Hello World");
}

function Student() {
    this.score = 0;
    let grade = 0;
    this.upGrade = () => grade++;
    this.getGrade = function() {
        return grade;
    }
    this.setGrade = (newGrade) => grade = newGrade;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

let student = new Student();
student.name = "Reyn";
student.age = 21;
student.score = 98.0;
student.setGrade("七");

console.log(student.name);  // Reyn
console.log(student.age);   // 21
console.log(student.getGrade());    // 七

此方式通过将实例对象 student 的构造函数 Student 的原型对象修改为 Person 类的一个实例对象,从而实现了继承,然而存在一个明显的缺陷,即不能在构造函数中初始化父类中的属性,在解决此问题之前,必须了解一些方法,均用于修改函数或方法中默认的 this 属性

在 JavaScript 中,函数或方法中默认的 this 属性均引用调用此函数或方法的对象,调用函数的对象默认为 Window 全局对象

  1. bind

将函数或方法中默认的 this 属性引用的对象绑定到 object,并返回一个新函数,此外,可以将一些额外的参数 args 传递给调用此方法的函数对象

function bind(object, args)

示例如下:

let obj = {
    label: "binded"
}

function demo(name) {
    console.log(name);
    console.log(this);
}

let fnc = demo.bind(obj, "Reyn");

fnc();  // Reyn `\n` {"label":"binded"}
  1. call

将函数或方法中默认的 this 属性引用的对象绑定到 object,并立即调用一次调用此方法的函数或方法,此外,可以将一些额外的参数 args 传递给调用此方法的函数对象

call(object, args)

示例如下:

let obj = {
    label: "binded"
}

function demo(name) {
    console.log(name);
    console.log(this);
}

demo.call(obj, "Reyn"); // Reyn `\n` {"label":"binded"}
  1. apply

将函数或方法中默认的 this 属性引用的对象绑定到 object,并立即调用一次调用此方法的函数或方法,此外,可以将一些额外的参数 args 传递给调用此方法的函数对象,参数 args 的形式必须是一个数组

apply(object, ...args)

示例如下:

let obj = {
    label: "binded"
}

function demo(name, age) {
    console.log(name, 21);
    console.log(this);
}

demo.apply(obj, ["Reyn", 21]); // Reyn,21 `\n` {"label":"binded"}

以下是实现继承的具体步骤:

  • 在子类中通过父类构造函数调用 call 传递子类的 this 属性和初始化信息
  • 修改子类的原型对象为父类的一个实例对象
  • 修改子类的原型对象的 constructor 属性为子类构造函数
function Person(oName, oAge) {
    this.name = oName;
    this.age = oAge;
}

Person.prototype.say = function() {
    console.log("Hello World");
}

function Sutdent(oName, oAge, oScore) {
    Person.call(this, oName, oAge);
    this.score = oScore;
    this.study = function() {
        console.log("Day Day Up!");
    }
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student("Reyn", 21, 98.0)
console.log(stu.name);  // Reyn
console.log(stu.age);   // 21
console.log(stu.say()); // Hello World
console.log(stu.score); // 98
console.log(stu.study());   // Day Day Up!

在实现继承的方式中,除了在子类中通过父类构造函数调用 call 传递子类的 this 属性和初始化信息之外,必须修改子类的原型对象,因为父类的某些方法可能作为公共方法存储在父类构造函数的原型对象中,如果不修改子类的原型对象,那么无法继承父类的所有属性和方法,不仅如此,子类的原型对象必须修改为父类的一个实例对象,否则,如果为子类的原型对象添加属性和方法时,父类的原型对象将会被破坏,此外,在修改子类的原型对象之后,切记将子类原型对象的 constructor 引用子类构造函数

多态

多态性即某一个对象在不同时刻可以表现出不同的状态,由于 JavaScript 是一种弱类型语言,在变量中存储什么类型的数据不是严格定义的,也就是说在不同的时刻本身就可以存储不同类型的数据,从而表现出不同的状态,即 JavaScript 中的变量本身就具有多态性,不需要太多关注

静态属性/方法

在 JavaScript 中,对象的属性和方法由实例和静态之分,通过实例对象调用的属性或方法一般称之为实例属性或实例方法,通过构造函数(构造函数在 JavaScript 中也是一个对象——函数对象)访问的属性或方法一般称之为静态属性或静态方法,简言之,属于每一个实例对象的属性或方法称之为实例属性或实例方法,属于一个构造函数的属性或方法一般称之为静态属性或静态方法,示例如下:

function Person(oName, oAge) {
    this.name = oName;
    this.age = oAge;
    this.say = () => console.log("Hello World");
}

let person = new Person("Reyn", 21);

console.log(person.name);   // Reyn
console.log(person.age);    // 21

Person.ID = 10010;

console.log(Person.ID); // 10010

ES6 类和对象

在 ES6 之前使用构造函数定义类,在 ES6 之后使用关键词 class 定义类,示例如下:

class Person {
    /* 构造方法 */
    constructor(oName, oAge) {
        /* 实例属性 */
        this.name = oName;
        this.age = oAge;
    }
    /* 实例方法 */
    say() {
        console.log("Hello World");
    }
    /* 静态属性 */
    static ID = 10010;
    /* 静态方法 */
    static printInfo() {
        console.log(ID);
    }
}
let person = new Person("Reyn", 21);
console.log(person.ID); // 10010
console.log(person.name);   // Reyn
console.log(person.age):    // 21
person.say();   // Hello World

在上述示例中,以 constructor 命名的函数被称为构造方法,当使用 new 运算符创建一个对象时,系统将自动调用此方法,此外,在 JavaScript 中,实例对象只能访问实例属性和调用实例方法,类只能访问静态属性和调用静态方法;在 ES6 标准中,实例属性必须写在构造方法中,而 static 关键字只能定义静态方法,而不能定义静态属性,如果要定义静态属性,必须在类外使用形如 Person.ID = 10010; 的方式;值得注意的是,在 class 中,写在 constructor 构造方法中的属性和方法存在于类的实例对象中(类似于写在 ES6 之前的构造函数),而不在 constructor 构造方法中的方法存在于类的原型对象中(类似于写在 ES6 之前的 prototype 引用的对象中),示例如下:

class Person {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
        this.say = function() {
            console.log("Hello World");
        }
    }
    printName() {
        console.log(this.name);
    }
    static printInfo() {
        console.log(ID);
    }
}

let person = new Person("Reyn", 21);

console.log(person);

如果使用 class 定义类,那么不能使用自定义原型对象的方式为类添加属性或方法,只能使用动态添加属性或方法的方式,原因在于如果使用自定义原型对象的方式,那么不在 constructor 中的方法将无法自动添加到原型对象上(被篡改了),示例如下:

class Person {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
    }
    say() {
        console.log("Hello World");
    }
}

// Person.prototype = {
//     constructor: Person,
//     ID: 10010,
//     hi: function() {
//         console.log("Hi! How are you?");
//     }
// }

Person.prototype.ID = 10010;
Person.prototype.printName = function() {
    console.log(this.name);
}

let person = new Person("Reyn", 21);

console.log(person);

ES6 继承

在 ES6 之后,使用 extends 关键字实现继承,示例如下:

class Person {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
    }
    say() {
        console.log("Hello World");
    }    
}

class Student extends Person {
    constructor(oName, oAge, oScore) {
        super(oName, oAge);
        this.score = oScore;
    }
    study() {
        console.log("Day Day Up!");
    }
}

let stu = new Student("Reyn", 21, 98.0);

console.log(stu.say());

对象类型

在 JavaScript 中,可以通过实例对象访问 constructor.name 属性获取对象类型,示例如下:

function Person(oName, oAge) {
    this.name = oName;
    this.age = oAge;
    this.say = () => console.log("Hello World");
}

let obj = new Object();
let arr = new Array();
let per = new Person();

console.log(typeof obj);    // Object
console.log(typeof arr);    // Object
console.log(typeof per);    // Object

console.log(obj.constructor.name);  // Object
console.log(arr.constructor.name);  // Array
console.log(per.constructor.name);  // Person

在上述示例中,使用 typeof 关键字时,之所以返回 Object 是因为在调用构造函数时实际是调用工厂函数,工厂函数默认在函数体开始时 new Object();通过实例对象访问 constructor 属性时,将访问实例对象构造函数的原型对象中的 constructor,由于 constructor 属性引用构造函数,因此通过 name 属性可以获取实际的对象类型

instanceof

在 JavaScript 中,关键字 instanceof 用于判断某个实例对象是否是某个构造函数的实例,示例如下:

function Person(oName) {
    this.name = oName;
}

function Animal(oType) {
    this.type = oType;
}

let per = new Person("Reyn");
let cat = new Animal("Cat");

console.log(per instanceof Person); // true
console.log(cat instanceof Person); // false

在使用 instanceof 关键字时,只要待检测构造函数在示例对象的原型链上,那么结果即为 true,示例如下:

function Person(oName) {
    this.name = oName;
}

function Student(oName, oScore) {
    Person.call(this, oName);
    this.score = oScore;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student("Reyn", 98);

console.log(stu instanceof Person); // true

isPrototypeOf

在 JavaScript 中,使用 isPrototypeOf 方法判断调用此方法的对象是否是某个实例对象的原型对象,示例服下:

function Person(oName) {
    this.name = oName;
}

function Animal(oType) {
    this.type = oType;
}

let per = new Person("Reyn");
let cat = new Animal("Cat");

console.log(Person.prototype.isPrototypeOf(per));   // true
console.log(Person.prototype.isPrototypeOf(cat));   // false

在使用 isPrototypeOf 方法时,只要待检测实例对象在原型对象所在的原型链上,那么结果即为 true,示例如下:

function Person(oName) {
    this.name = oName;
}

function Student(oName, oScore) {
    Person.call(this, oName);
    this.score = oScore;
}

Student.prototype = new Person();
Student.prototype.constructor = Student;

let stu = new Student("Reyn", 98);

console.log(Person.prototype.isPrototypeOf(stu)); // true

in

在 JavaScript 中,使用 in 运算符判断某个属性或方法是否存在于某个实例对象或类的原型对象中,示例如下:

class Person {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
    }
    say() {
        console.log("Hello World");
    }
}

let person = new Person("Reyn", 21);

console.log("name" in person);  // true
console.log("score" in person); // false
console.log("say" in person);   // true

hasOwnProperty

在 JavaScript 中,通过某个实例对象调用 hasOwnProperty 方法检测某个属性或方法是否存在于实例对象自身,示例如下:

class Person {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
    }
    say() {
        console.log("Hello World");
    }
}

let person = new Person("Reyn", 21);

console.log(person.hasOwnProperty("name"));  // true
console.log(person.hasOwnProperty("score")); // false
console.log(person.hasOwnProperty("say"));   // false

访问属性

在 JavaScript 中,除了可以使用 . 运算符访问属性之外,也可以使用 [] 运算符访问属性,示例如下:

class Person {};
let person = new Person();

person.name = "Reyn";
person["age"] = 21;
person.say = () => console.log("Hello World");
person["printName"] = () => console.log(this.name);

console.log(person);    // {"name":"Reyn","age":21}

delete person.age;
delete person["printName"];

console.log(person);    // {"name":"Reyn"}

person["name"] = "Jobs";

console.log(person["name"]);    // Jobs

person.say();   // Hello World

遍历属性

在 JavaScript 中,可以使用高级 for 循环遍历所有属于实例对象而非原型对象的属性,示例如下:

class Person {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
    }
    say() {
        console.log("Hello World");
    }
}

let per = new Person("Reyn", 21);

for (key in per) {
    console.log(per[key]);  // Reyn '\n' 21
}

class Student {
    constructor(oName, oAge) {
        this.name = oName;
        this.age = oAge;
        this.say = () => console.log("Hello World");
    }
}

let stu = new Student("Reyn", 21);

for (key in stu) {
    console.log(stu[key]);  // Reyn '\n' 21 `\n` () => console.log("Hello World")
}

在 ES6 中,如果方法不在 constructor 中,则方法默认在类的原型对象中,而使用高级 for 循环遍历属性时,不会遍历类原型对象中的属性,此外,在 for 循环中访问属性时,必须使用 [] 运算符,而不能使用 . 运算符

解构赋值

在 JavaScript 中,实例对象的解构赋值与数组的解构赋值相似,区别在于符号不同,数组使用 [] 解构赋值,对象使用 {} 解构赋值,示例如下:

let obj = {
    name: "Reyn",
    age: 21
}

// let {name, age} = obj;
// console.log(name, age); // Reyn,21

// let {name, age} = {name: "Reyn", age: 21};
// console.log(name, age); // Reyn,21

// let {name} = {name: "Reyn", age: 21};
// console.log(name);  // Reyn

// let {name, age, height} = {name: "Reyn", age: 21};
// console.log(name, age, height); // Reyn,21,undefined

let {name, age, height = 1.70} = {name: "Reyn", age: 21};
console.log(name, age, height); // Reyn,21,1.7

在实例对象的解构赋值中,赋值运算符左侧的变量名称必须与对象中的属性名称相同才可以解构,示例如下:

let obj = {
    name: "Reyn",
    age: 21
}
let {a, b} = obj;
console.log(a, b);  //  undefined,undefined

不论顺序如何,系统总是可以找到变量相应的属性解构,示例如下:

let {age, name} = {name: "Reyn", age: 21};
console.log(name, age); // Reyn,21

解构赋值通常应用在函数形参的传递,示例如下:

let sum = ([a, b]) => a + b;
let arr = [5, 3];
console.log(sum(arr));  // 8

let say = ({name, age}) => console.log(name, age);
let obj = {
    name: "Reyn",
    age: 21
}
say(obj);   // Reyn,21

拷贝

在 JavaScript 中,拷贝分为深拷贝与浅拷贝,深拷贝是指将一个变量赋值给另一个变量之后,修改新变量的值不会影响原变量的值,默认情况下基本数据类型均是深拷贝,浅拷贝是指将一个变量赋值给另一个变量之后,修改新变量的值将影响原变量的值,默认情况下引用数据类型均是浅拷贝

深拷贝

可以通过高级 for 循环遍历对象属性的方式将一个引用类型的变量中的属性拷贝到另一个引用类型的变量中,示例如下:

let obj1 = {
    name: "Reyn",
    age: 21
}

let obj2 = new Object();
for (key in obj1) {
    obj2[key] = obj1[key];
}

console.log(obj2);  // {"name":"Reyn","age":21}

obj2["name"] = "Jobs";
console.log(obj1.name); // Reyn
console.log(obj2.name); // Jobs

也可以通过 Object 类的 assign 方法,将一个引用类型变量的属性拷贝到另一个引用类型的变量中,示例如下:

let obj1 = {
    name: "Reyn",
    age: 21
}

let obj2 = new Object();
Object.assign(obj2, obj1);

console.log(obj2);  // {"name":"Reyn","age":21}

obj2["name"] = "Jobs";
console.log(obj1.name); // Reyn
console.log(obj2.name); // Jobs

上述的两种方法并不能实现真正的深拷贝,如果被拷贝的对象中的某些属性任然是引用类型,那么仅仅拷贝类型显然是不够的,此时,我们必须自定义深拷贝方法,示例如下:

let obj1 = {
    name: {
        firstname: "Reyn",
        lastname: "Morales"
    },
    age: 21,
    scores: [95, 98, 92]
}

let obj2 = new Object();

function deepCopy(targetRef, sourceRef) {
    for (key in sourceRef) {
        sourceValue = sourceRef[key];
        if (sourceValue instanceof Object) {
            targetRef[key] = new sourceValue.constructor;
            deepCopy(targetRef[key], sourceValue);
        } else {
            targetRef[key] = sourceValue
        }
    }
}

deepCopy(obj2, obj1);

console.log(obj1);  // {"name":{"firstname":"Reyn","lastname":"Morales"},"age":21,"scores":[95,98,92]}
console.log(obj2);  // {"name":{"firstname":"Reyn","lastname":"Morales"},"age":21,"scores":[95,98,92]}

obj2.name.firstname = "Steven";
obj2.name.lastname = "Randy";
obj2["age"] = 23;

console.log(obj1);  // {"name":{"firstname":"Reyn","lastname":"Morales"},"age":21,"scores":[95,98,92]}
console.log(obj2);  // {"name":{"firstname":"Steven","lastname":"Randy"},"age":23,"scores":[95,98,92]}

在函数 deepCopy 中遍历 sourceRef 中的每一个属性,判断属性是否为引用类型,如果不是引用类型,那么可以直接拷贝,如果是引用类型,那么需要通过 sourceValue 的 constructor 属性(原型链)获得其引用类型(构造函数),通过 new 运算符使用此构造函数开辟内存空间,然后以递归的形式填充此内存空间

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值