JavaScript 构造函数及__proto__、prototype 属性

150 阅读5分钟

1. JS 构造函数的演化史

对象需要分类吗?这个问题值得思考,下面我们通过一个实例来看看。

假如客户需要一个正方形,它要有边长、面积和周长,我们很快写出了如下代码:

let square = {
    width: 5,
    getArea() {
        return this.width * this.width;
    },
    getLength() {
        return this.width * 4;
    }
}

这时客户又提出需要 12 个正方形,完善代码:

let squareList = [];
for(let i = 0; i < 12; i++) {
    squareList[i] = {
        width: 5,
        getArea() {
            return this.width * this.width;
        },
        getLength() {
            return this.width * 4;
        }
    }
}

如果这 12 个正方形的边长不同呢?

let squareList = [];
let widthList = [7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8];
for(let i = 0; i < 12; i++) {
    squareList[i] = {
        width: widthList[i],
        getArea() {
            return this.width * this.width;
        },
        getLength() {
            return this.width * 4;
        }
    }
}

以上代码可以获得不同边长的正方形,但性能不好,十分浪费内存。在循环时每次都会新生成 getArea 和 getLength,创建新的函数。两队函数各重复了 11 次。

内存图

继续优化,借助原型,将 12 个对象的共同属性放到原型里。

let squareList = [];
let widthList = [7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8];
let squarePrototype = {
    getArea() {
        return this.width * this.width;
    },
    getLength() {
        return this.width * 4;
    }
}
for(let i = 0; i < 12; i++) {
    squareList[i] = Object.create(squarePrototype);
    squareList[i].width = widthList[i];
}

通过调整,代码好了很多,但还是太分散了。下面尝试把代码抽离到一个函数里,然后调用函数。

let squareList = [];
let widthList = [7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8];
// 这个函数就是构造函数
function createSquare(width) {
    let obj = Object.create(squarePrototype);
    obj.width = width;
    return obj;
}
let squarePrototype = {
    getArea() {
        return this.width * this.width;
    },
    getLength() {
        return this.width * 4;
    }
}
for(let i = 0; i < 12; i++) {
    squareList[i] = createSquare(widthList[i]);
}

上面代码还有问题,squarePrototype 原型和 createSquare 构造函数还是分散的,缺少某种关系,下面结合函数和原型。

let squareList = [];
let widthList = [7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8];
// 这个函数就是构造函数
function createSquare(width) {
    // 这段代码调用时才执行,不属于先使用再定义
    let obj = Object.create(createSquare.squarePrototype);
    obj.width = width;
    return obj;
}
// 把原型挂到函数上
createSquare.squarePrototype = {
    getArea() {
        return this.width * this.width;
    },
    getLength() {
        return this.width * 4;
    },
    // 方便通过原型找到构造函数
    constructor: createSquare
}
for(let i = 0; i < 12; i++) {
    squareList[i] = createSquare(widthList[i]);
    // 可以知道谁构造了这个对象
    console.log(squareList[i].constructor);
}

这段代码基本上已经完美了,JS 之父为了便于我们操作,用 new 操作符简化逻辑,固化了这段代码。

let squareList = [];
let widthList = [7, 8, 7, 8, 7, 8, 7, 8, 7, 8, 7, 8];
// 构造函数
function Square(width) {
    this.width = width;
}
// 这里不能直接对 prototype 赋值,因为已经有了一个属性constructor,重新赋值constructor会被覆盖
// 可以用 assign 批量添加属性
Square.prototype.getArea = function() {
    return this.width * this.width;
}
Square.prototype.getLength = function() {
    return this.width * 4;
}
for(let i = 0; i < 12; i++) {
    squareList[i] = createSquare(widthList[i]);
    console.log(squareList[i].constructor);
}

使用 new 操作符,构造小狗对象

function Dog(name, color, kind) {
    this.name = name;
    this.color = color;
    this.kind = kind;
}
Dog.prototype.bark = function() {
    console.log('汪汪');
}
Dog.prototype.run = function () {
    console.log('狗在跑')
}
let dog1 = new Dog('旺财', '橡木色', '萨摩耶');
dog1.bark();
dog1.run();

2. JS 构造函数

new X() 做了哪些事

  • 自动创建空对象
  • 自动为空对象关联原型,原型地址指定为 X.prototype
  • 自动将空对象作为 this 关键字运行构造函数
  • 自动 return this

构造函数 X

  • X 函数本身负责给对象添加属性
  • X.prototype 对象负责保存对象的共同属性

命名规范

所有构造函数(专门用于创建对象的函数)首字母大写,所有被构造函数构造出来的对象首字母小写。new 后面的函数使用名词形式,如 new Person。其他函数一般用动词命名。

他人创建的构造函数的使用

JS 没有办法直接告诉你别人写的构造函数要传递几个参数,只有查看文档,如果没有文档只能看源码。

3. ES6 新语法 class

JS 构造对象目前有两种方式,一种是用构造函数+prototype,一种是用 class。构造函数+prototype 是先提供的,class 是后提供的。构造函数+prototype 是 JS 一开始的基因。

class Square {
    constructor(width) {
        this.width = width;
    }
    getArea() {
        return this.width * this.width;
    },
    getLength() {
        return this.width * 4;
    }
}

用 class 语法改写“人”类

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.sayHi = function() {
  console.log(`你好,我叫${this.name}`)
}

let person = new Person('frank', 18);
let person2 = new Person('jack', 19);
person.name === 'frank';
person.age === 18; 
person.sayHi(); // 你好,我叫frank

person2.name === 'jack';
person2.age === 19;
person2.sayHi();
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    sayHi() {
        console.log(`你好,我叫${this.name}`);
    }
}

4. JS 对象的分类

  • 类型是对 JS 数据的分类。类是针对于对象数据类型的分类,有无数种,如 Array、Function、Date、RegExp等。
  • window 是 Window 构造的。
  • window.Object 是 window.Function 构造的。因为所有函数都是 window.Function 构造的。
  • window.Function 是自己构造的。实际是浏览器构造的,然后指定它的构造者是它自己。

5. __proto__ 、prototype 属性

  • 通用公式:对象.__proto__ === 对象的构造函数.prototype。你是谁构造的,你的原型就是谁的 prototype 属性对应的对象。prototype 不是原型,它只是存了原型的地址。
  • Object.prototye 是「Object 构造出来的对象 obj」的原型,即 obj.__proto__ === Object.prototype。Object.prototye 不是 Object 的原型,Object.__proto__ 才是 Object 的原型。
  • 所有函数一出生就有一个 prototype 属性,所有 prototype 一出生就有一个 constructor 属性,所有 constructor 属性一出生就保存了对应的函数的地址。如果一个函数不是构造函数,它依然拥有 prototype 属性,只不过这个属性暂时没什么用。如果一个对象不是函数,那么这个对象一般来说没有 prototype 属性,但这个对象一般一定会有 __proto__ 属性。

6. 面试题

请问:

let x = {}

(1)x 的原型是什么?

(2)x.__proto__ 的值是什么?

(3)上面两个问题是等价的吗?

(4)请用内存图画出 x 的所有属性

(1) let x = {}; 是 let x = new Object(); 的简写。所以 x 的原型是 Object.prototype 对应的对象。
(2) x.__proto__ === Object.prototype,值为 x 原型的地址,即 Object.prototype 对应的对象。
(3) 是等价的。

内存图

请问:

(1)Object.prototype 是哪个构造函数构造出来的?

(2)Object.prototype 的原型是什么?

(3)Object.prototype.proto?

(1) 没有爸爸没有妈妈,答案是不知道
(2) 没有原型
(3) null