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