




















一、从对象说起, 一切皆对象在 JS 中, 几乎所有的东西都是对象, 或者说最终都会指向某个对象. 比如:
let arr = [1, 2, 3];console.log(typeof arr);function foo() {}console.log(typeof foo);
简单的对象创建:
let person = {name: '张三',age: 25,sayHi: function() {console.log('你好,我是' + this.name);}};person.sayHi(); // 你好, 我是张三
但这样有个问题——每次创建新对象都要写一遍所有属性和方法, 太麻烦了! 于是我们有了后面的构造函数. 二、构造函数,对象的工厂
function Person(name, age) {this.name = name;this.age = age;this.sayHi = function() {console.log('你好, 我是' + this.name);};}let person1 = new Person('张三', 25);let person2 = new Person('李四', 30);person1.sayHi(); // 你好, 我是张三person2.sayHi(); // 你好, 我是李四
这里有几个关键点:
1. 函数名通常大写开头,这是约定俗成的构造函数命名方式
2. 使用 new 关键字调用
3. 函数内部使用 this 来指代新创建的对象
当你使用 new 调用函数时,背后发生了这些事:
1. 创建一个全新的空对象
2. 将这个空对象的 __proto__ 指向构造函数的 prototype 属性
3. 将构造函数中的 this 绑定到这个新对象
4. 执行构造函数中的代码
5. 如果构造函数没有显式返回对象,则返回这个新对象
三、原型登场
上面的构造函数方式有个问题——每个实例都会创建自己的方法副本, 浪费内存, 这时候,原型(prototype)就派上用场了!
什么是原型?
每个函数都有一个 prototype 属性, 它指向一个对象. 当使用这个函数作为构造函数创建实例时, 所有实例都会共享这个原型对象上的属性和方法.
改进版的 Person:
function Person(name, age) {this.name = name;this.age = age;}// 将方法放在原型上Person.prototype.sayHi = function() {console.log('你好,我是' + this.name);};let person1 = new Person('张三', 25);let person2 = new Person('李四', 30);console.log(person1.sayHi === person2.sayHi);
什么是原型链?
当我们访问一个对象的属性时, JavaScript 会:
1. 先在对象自身属性中查找
2. 如果没找到, 就去对象的 __proto__ (即构造函数的 prototype) 中查找
3. 如果还没找到, 就继续往 __proto__.__proto__ 中查找
4. 直到找到 Object.prototype (最顶层的原型,它的 __proto__ 是 null)
这就是原型链, JavaScript的继承机制.
function Person() {}Person.prototype.species = '人类';let p = new Person();console.log(p.species); //"人类" - 从原型上找到的console.log(p.__proto__ === Person.prototype);console.log(Person.prototype.__proto__ === Object.prototype);console.log(Object.prototype.__proto__);
如果对象自身有属性, 就不会去原型上找了:
p.species = '地球人';console.log(p.species); //"地球人" - 自身的属性console.log(Person.prototype.species);delete p.species;console.log(p.species); //"人类" - 删除自身属性后又从原型上找到了
四、几种继承的方式
1)原型链继承:
function Parent() {this.parentProp = '父类属性';}Parent.prototype.parentMethod = function() {console.log('父类方法');};function Child() {this.childProp = '子类属性';}// 关键点: 子类的原型指向父类的实例Child.prototype = new Parent();let child = new Child();console.log(child.childProp); // "子类属性"console.log(child.parentProp); // "父类属性"-通过原型链找到的child.parentMethod(); // "父类方法"
问题:
所有子类实例共享同一个父类实例的属性(如果是引用类型就麻烦了)
创建子类实例时无法向父类构造函数传参
2)构造函数继承:
function Parent(name) {this.name = name;this.colors = ['red', 'blue'];}function Child(name, age) {Parent.call(this, name); //关键点:在子类构造函数中调用父类构造函数this.age = age;}let child1 = new Child('张三', 25);child1.colors.push('green');let child2 = new Child('李四', 30);console.log(child2.colors); //["red", "blue"]-不受child1影响
优点:
1. 避免了引用类型属性被所有实例共享
2. 可以在子类中向父类传参
缺点:
方法都在构造函数中定义,每次创建实例都会创建一遍方法
不能继承父类原型上的方法
3)组合继承:
结合原型链继承和构造函数继承的优点:
function Parent(name) {this.name = name;this.colors = ['red', 'blue'];}Parent.prototype.sayName = function() {console.log(this.name);};function Child(name, age) {Parent.call(this, name); //第二次调用Parentthis.age = age;}Child.prototype = new Parent();Child.prototype.constructor = Child;Child.prototype.sayAge = function() {console.log(this.age);};let child1 = new Child('张三', 25);child1.colors.push('green');child1.sayName(); //"张三"child1.sayAge(); //25let child2 = new Child('李四', 30);console.log(child2.colors); //["red", "blue"]
4)寄生组合继承:
function inheritPrototype(child, parent) {let prototype = Object.create(parent.prototype);//Object.create 是创建以指定对象为原型的新对象的方法prototype.constructor = child; // 修复constructorchild.prototype = prototype; // 将副本作为子类的原型}function Parent(name) {this.name = name;this.colors = ['red', 'blue'];}Parent.prototype.sayName = function() {console.log(this.name);};function Child(name, age) {Parent.call(this, name);this.age = age;}// 关键: 使用我们封装的inheritPrototype函数inheritPrototype(Child, Parent);Child.prototype.sayAge = function() {console.log(this.age);};let child = new Child('张三', 25);child.sayName(); // "张三"child.sayAge(); // 25
优点:
1. 只调用一次父类构造函数
2. 子类原型上不会有多余的父类属性
3. 原型链保持不变
五、ES6的class
ES6 引入了 class 语法, 让面向对象编程更加直观:
class Parent {constructor(name) {this.name = name;this.colors = ['red', 'blue'];}sayName() {console.log(this.name);}}class Child extends Parent {constructor(name, age) {super(name); // 必须在使用this之前调用superthis.age = age;}sayAge() {console.log(this.age);}}let child = new Child('张三', 25);child.sayName(); // "张三"child.sayAge(); // 25
class 本质上仍然是基于原型的语法糖. 上面的代码大致相当于:
function Parent(name) {this.name = name;this.colors = ['red', 'blue'];}Parent.prototype.sayName = function() {console.log(this.name);};function Child(name, age) {Parent.call(this, name);this.age = age;}Child.prototype = Object.create(Parent.prototype);Child.prototype.constructor = Child;Child.prototype.sayAge = function() {console.log(this.age);};
class 还提供了静态方法和属性的简洁写法:
class MyClass {static staticMethod() {console.log('我是静态方法');}static staticProperty = '我是静态属性';}MyClass.staticMethod(); //"我是静态方法"console.log(MyClass.staticProperty);
六、其他相关内容
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上.
function myInstanceof(instance, constructor) {let proto = Object.getPrototypeOf(instance);while (proto) {if (proto === constructor.prototype) {return true;}proto = Object.getPrototypeOf(proto);}return false;}console.log([] instanceof Array);console.log(myInstanceof([], Array));
原型可以被修改, 这可能导致安全问题:
// 不要这样做!Object.prototype.hack = function() {console.log('被黑了!');};let obj = {};obj.hack(); // "被黑了!"
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。