由于 JavaScript(ES5) 缺乏类, 但可用构造函数和原型对象给对象带来与类相似的功能.
4.1 构造函数
构造函数的函数名首字母应大写, 以此区分其他函数.
当没有需要给构造函数传递参数, 可忽略小括号:
- var Person = {
- // 故意留空
- }
- var person = new Person;
尽管 Person 构造函数没有显式返回任何东西, 但 new 操作符会自动创建给定类型的对象并返回它们.
每个对象在创建时都自动拥有一个构造函数属性(constructor, 其实是它们的原型对象上的属性), 其中包含了一个指向其构造函数的引用.
通过对象字面量形式 ({}) 或 Object 构造函数创建出来的泛用对象, 其构造函数属性 (constructor) 指向 Object; 而那些通过自定义构造函数创建出来的对象, 其构造函数属性指向创建它的构造函数.
- console.log(person.constructor === Person); // true
- console.log(({
- }).constructor === Object); // true
- console.log(([1,2,3]).constructor === Object); // true
- // 证明 constructor 是在原型对象上
- console.log(person.hasOwnPrototype("constructor")); // false
- console.log(person.constructor.prototype.hasOwnPrototype("constructor")); // true
尽管对象实例及其构造函数之间存在这样的关系, 但还是建议使用 instanceof 来检查对象类型. 这是因为构造函数属性可以被覆盖.(person.constructor = "").
当你调用构造函数时, new 会自动自动创建 this 对象, 且其类型就是构造函数的类型(构造函数就像类, 相当于一种数据类型).
你也可以在构造函数中显式调用 return. 如果返回值是一个对象, 它会代替新创建的对象实例而返回, 如果返回值是一个原始类型, 它会被忽略, 新创建的对象实例会被返回.
始终确保要用 new 调用构造函数; 否则, 你就是在冒着改变全局对象的风险, 而不是创建一个新的对象.
- var person = Person("Nicholas"); // 缺少 new
- console.log(person instanceof Person); // false
- console.log(person); // undefined, 因为没用 new, 就相当于一个普通函数, 默认返回 undefined
- console.log(name); // "Nicholas"
当 Person 不是被 new 调用时, 构造函数中的 this 对象等于全局 this 对象.
在严格模式下, 会报错. 因为严格模式下, 并没有为全局对象设置 this,this 保持为 undefined.
以下代码, 通过 new 实例化 100 个对象, 则会有 100 个函数做相同的事. 因此可用 prototype 共享同一个方法会更高效.
- var person = {
- name: "Nicholas",
- sayName: function(){
- console.log(this.name);
- }
- }
4.2 原型对象
可以把原型对象看作是对象的基类. 几乎所有的函数 (除了一些内建函数) 都有一个名为 prototype 的属性, 该属性是一个原型对象用来创建新的对象实例.
所有创建的对象实例 (同一构造函数, 当然, 可能访问上层的原型对象) 共享该原型对象, 且这些对象实例可以访问原型对象的属性. 例如, hasOwnProperty() 定义在 Object 的原型对象中, 但却可被任何对象当作自己的属性访问.
- var book = {
- title : "book_name"
- }
- "hasOwnProperty" in book; // true
- book.hasOwnProperty("hasOwnProperty"); // false
- Object.property.hasOwnProperty("hasOwnProperty"); // true
鉴别一个原型属性
- function hasPrototypeProperty(object, name){
- return name in object && !object.hasOwnProperty(name);
- }
4.2.1 [[Prototype]] 属性
一个对象实例通过内部属性 [[Prototype]] 跟踪其原型对象.
该属性是一个指向该实例使用的原型对象的指针. 当你用 new 创建一个新的对象时, 构造函数的原型对象就会被赋给该对象的 [[Prototype]] 属性.
Object.getPrototypeOf() 方法可读取 [[Prototype]] 属性的值.
- var obj = {
- };
- var prototype = Object.getPrototypeOf(Object);
- console.log(prototype === Object.prototype); // true
大部分 JavaScript 引擎在所有对象上都支持一个名为 __proto__ 的属性. 该属性使你可以直接读写 [[Prototype]] 属性.
isPrototypeOf() 方法会检查某个对象是否是另一个对象的原型对象, 该方法包含在所有对象中.
- var obj = {
- }
- console.log(Object.prototype.isPrototypeOf(obj)); // true
当读取一个对象的属性时, JavaScript 引擎首先在该对象的自有属性查找属性名. 如果找到则返回. 否则会搜索 [[Prototype]] 中的对象, 找到则返回, 找不到则返回 undefined.
- var obj = new Object();
- console.log(obj.toString()); // "[object Object]"
- obj.toString = function(){
- return "[object Custom]";
- }
- console.log(obj.toString()); // "[object Custom]"
- delete obj.toString; // true
- console.log(obj.toString()); // "[object Object]"
- delete obj.toString; // 无效, delete 不能删除一个对象从原型继承而来的属性
- cconsole.log(obj.toString()); // // "[object Object]"
delete 操作符不能删除的属性有:1显式声明的全局变量不能被删除, 该属性不可配置(not configurable); 2内置对象的内置属性不能被删除; 3不能删除一个对象从原型继承而来的属性(不过你可以从原型上直接删掉它).
一个重要概念: 无法给一个对象的原型属性赋值. 但我们可以通过 obj.constructor.prototype.sayHi = function(){console.log("Hi!")} 向原型对象添加属性.
(图片中间可以看出, 为对象 obj 添加的 toString 属性代替了原型属性)
4.2.2 在构造函数中使用原型对象
原型对象的共享机制使得它们成为一次性为所有对象定义所有方法的理想手段, 因为一个方法对所有的对象实例做相同的事, 没理由每个实例都要有一份自己的方法.
将方法放在原型对象中并使用 this 方法当前实例是更有效的做法.
- function Person(name) {
- this.name = name
- }
- Person.prototype.sayName = function() {
- console.log(this.name)
- };
- var person1 = new Person("Nicholas")
- console.log(person1.name) // Nicholas
- person1.sayName() // Nicholas
也可以在原型对象上存储其他类型的数据, 但是在存储引用值时要注意, 因为这些引用值会被多个实例共享, 可能大家不希望一个实例能够改变另一个实例的值.
- function Person(name) {
- this.name = name
- }
- Person.prototype.favorites = []
- var person1 = new Person("Nicholas")
- var person2 = new Person("Greg")
- person1.favorites.push("pizza")
- person2.favorites.push("quinoa")
- console.log(person1.favorites) // ["pizza", "quinoa"]
- console.log(person2.favorites) // ["pizza", "quinoa"]
favorites 属性被定义到原型对象上, 意味着 person1.favorites 和 person2.favorites 指向同一个数组, 你对任意 Person 对象的 favorites 插入的值都将成为原型对象上数组的元素. 也可以使用字面量的形式替换原型对象:
- function Person(name) {this.name=name}
- Person.prototype= {
- sayName: function() {console.log(this.name)},
- toString: function(){return `[Person ${this.name} ]`}
- }
虽然用这种字面量的形式定义原型非常简洁, 但是有个副作用需要注意.
- var person1 = new Person('Nicholas')
- console.log(person1 instanceof Person) // true
- console.log(person1.constructor === Person) // false
- console.log(person1.constructor === Object) // true
使用字面量形式改写原型对象改写了构造函数的属性, 因此现在指向 Object 而不是 Person, 这是因为原型对象具有个 constructor 属性, 这是其他对象实例所没有的. 当一个函数被创建时, 其 prototype 属性也被创建, 且该原型对象的 constructor 属性指向该函数自己, 当使用字面量形式改写原型对象 Person.prototype 时, 其 constructor 属性将被复写为泛用对象 Object. 为了避免这一点, 需要在改写原型对象时手动重置其 constructor 属性:
- function Person(name) {this.name = name}
- Person.prototype = {
- constructor: Person, // 为了不忘记赋值, 最好在第一个属性就把 constructor 重置为自己
- sayName() {console.log(this.name)},
- toString() {return `[Person ${this.name} ]`}
- }
- var person1 = new Person('Nicholas')
- console.log(person1 instanceof Person) // true
- console.log(person1.constructor === Person) // true
- console.log(person1.constructor === Object) // false
构造函数, 原型对象, 对象实例之间: 对象实例和构造函数之间没有直接联系. 不过对象实例和原型对象之间以及原型对象和构造函数之间都有直接联系.
这样的连接关系也意味着, 如果打断对象实例和原型对象之间的联系, 那么也将打断对象实例及其构造函数之间的关系.
4.2.3 改变原型对象
因为每个对象的 [[Prototype]] 只是一个指向原型对象的指针, 所以原型对象的改动会立刻反映到所有引用它的对象.
当对一个对象使用封印 Object.seal() 或冻结 Object.freeze() 时, 完全是在操作对象的自有属性, 但任然可以通过在原型对象上添加属性来扩展这些对象实例.
4.2.4 内建对象 (如 Array,String) 的原型对象
所有内建对象都有构造函数, 因此也都有原型对象可以去改变, 例如要在数组上添加一个新的方法只需要改变 Array.prototype 即可
- Array.prototype.sum = function() {
- return this.reduce((privious, current) => privious + current)
- }
- var numbers = [1, 2, 3, 4, 5, 6]
- var result = numbers.sum()
- console.log(result) // 21
sum()函数内部, 在调用时 this 指向数组的对象实例 numbers, 因此 this 也可以调用该数组的其他方法, 比如 reduce(). 改变原始封装类型的原型对象, 就可以给这些原始值添加更多功能, 比如:
- String.prototype.capitalize = function() {
- return this.charAt(0).toUpperCase() + this.substring(1)
- }
- var message = 'hello world!'
- console.log(message.capitalize()) // Hello world!
总结
构造函数就是用 new 操作符调用的普通函数. 可用过 instanceof 操作符或直接访问 constructor(实际上是原型对象的属性) 来鉴别对象是被哪个构造函数所创建的.
每个函数都有一个 prototype 对象, 它定义了该构造函数创建的所有对象共享的属性. 而 constructor 属性实际上是定义在原型对象里, 供所有对象实例共享.
每个对象实例都有 [[Prototype]] 属性, 它是指向原型对象的指针. 当访问对象的某个属性时, 先从对象自身查找, 找不到的话就到原型对象上找.
内建对象的原型对象也可被修改
来源: https://juejin.im/post/5c15c8edf265da610f639eae