红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 前端学习笔记
  3. 正文

JavaScript 学习笔记 5:原型

2026年4月7日 4点热度 0人点赞 0条评论

原型

如果你需要创建一个对象,但不希望从头创建,而是利用一个已存在的对象创建,只是修改(扩展)其部分属性,这种情况下可以使用原型继承的方式实现:

let person = {
    name: 'Jonh',
    age: 20
}
​
let teacher = {
    school: 'MIT',
    __proto__: person
}
​
console.log(teacher.name);
console.log(teacher.age);
console.log(teacher.school)
// Jonh
// 20
// MIT

这里的__proto__是一个特殊的访问器属性,可以利用它设置对象的原型属性[[prototype]],当一个对象的原型属性是另一个对象时,访问这个对象的属性时如果发现该属性不存在,就会在它的原型属性中查找。

可以存在多层原型继承,形成一个原型链:

let animal = {
    eat() {
        console.log('Eat');
    }
};
​
let rabbit = {
    jump() {
        console.log('Jump');
    },
    __proto__: animal
}
​
let longEarRabbit = {
    __proto__: rabbit
}
​
longEarRabbit.eat();
longEarRabbit.jump();

修改属性

如果修改由原型继承的属性:

let animal2 = {
    eat() {
        console.log('Eat');
    }
};
​
let rabbit2 = {
    __proto__: animal2
}
​
rabbit2.eat = function() {
    console.log('Eat2');
}
​
rabbit2.eat();
animal2.eat();
// Eat2
// Eat

实际上是为当前对象创建一个新属性,并不会影响到原型对象的属性。

访问器属性略有不同,因为访问器属性的读写实际上是方法调用,而非简单的属性赋值:

let person2 = {
    firstName: 'Jonh',
    lastName: 'Doe',
    get fullName() {
        return `${this.firstName} ${this.lastName}`
    },
    set fullName(value) {
        let parts = value.split(' ');
        this.firstName = parts[0];
        this.lastName = parts[1];
    }
}
let teacher2 = {
    school: 'MIT',
    __proto__: person2
}
teacher2.fullName = 'Mike Smith';
console.log(teacher2.fullName);
console.log(person2.fullName);
// Mike Smith
// Jonh Doe

这里有个细节,通过访问器属性修改this.firstName与this.lastName的值时,this指向的是访问器属性调用时的对象,因此这里只有teacher2的相应属性被修改,而person2的属性不变。

for...in

通过原型继承的属性,会在 for...in 时被遍历:

let person3 = {
    name: 'Jonh',
    age: 20,
}
​
let teacher3 = {
    school: 'MIT',
    __proto__: person3
}
​
console.log(Object.keys(teacher3));
// [ 'school' ]
for(let key in teacher3) {
    console.log(key);
}
// school
// name
// age

可以用一个hasOwnProperty方法区分哪些属性是自己拥有的,哪些是从原型继承的:

for(let key in teacher3) {
    if(teacher3.hasOwnProperty(key)) {
        console.log('Own property: ' + key);
    }
    else {
        console.log('Inherited property: ' + key);
    }
}
// Own property: school
// Inherited property: name
// Inherited property: age

prototype

let basePerson = {
    toString() {
        return `${this.name} ${this.age}`;
    }
}
​
let Person = function(name, age) {
    this.name = name;
    this.age = age;
}
​
Person.prototype = basePerson;
let person4 = new Person('Jonh', 20);
console.log(person4.toString());
// Jonh 20
console.log(person4.__proto__ === Person.prototype)
// true

在这个示例中,利用new Person创建了一个新对象,不同的是,这里为Person函数添加了一个普通属性prototype,此时新创建的对象person4会自动通过原型继承的方式指向Person函数的prototype属性。

这里的prototype是一个普通属性,仅用于为新对象设置原型继承,与前面提到的原型继承特殊属性[[prototype]]没有关系。

实际上,函数默认存在一个prototype属性,它有一个constructor属性,指向函数自己:

let Person5 = function(name, age) {
    this.name = name;
    this.age = age;
}
console.log(Person5.prototype.constructor === Person5);

可以利用这一点很容易地获取到创建对象的构造函数,并创建新对象:

let Person5 = function(name, age) {
    this.name = name;
    this.age = age;
}
console.log(Person5.prototype.constructor === Person5);
​
let person5 = new Person5('Jonh', 20);
let person6 = new person5.constructor('Mike', 30);
console.log(person6.name);
// Mike

这存在一些缺陷,比如就像前面展示的,new方法的prototype属性可以随便重写覆盖,如果覆盖后的对象中不包含constructor属性或者该属性并不指向new方法自身,上面的使用方式就没有意义。

因此,如果需要修改new方法的prototype属性,更推荐的方式是不要覆盖,仅对默认属性进行扩展:

let Person7 = function (name, age) {
    this.name = name;
    this.age = age;
}
Person7.prototype.toString = function () {
    return `${this.name} ${this.age}`;
}
let person7 = new Person7('Jonh', 20);
console.log(person7.toString());

或者在覆盖的时候正确设置constructor:

let Person8 = function (name, age) {
    this.name = name;
    this.age = age;
}
​
Person8.prototype = {
    constructor: Person8,
    toString() {
        return `${this.name} ${this.age}`;
    }
}
​
let person8 = new Person8('Jonh', 20);
console.log(person8.toString());

原生原型

即使我们创建的是一个空对象,它也可以使用toString方法:

let obj = {};
console.log(obj.toString());
// [object Object]

实际上obj={}相当于obj2 = new Object(),借助前面说到的内容,很容易猜想到toString方法是来自Objeect.prototype指向的对象:

let obj2 = new Object();
console.log(obj2.__proto__ === Object.prototype);
console.log(obj2.toString === obj2.__proto__.toString);
console.log(obj2.toString === Object.prototype.toString);
// true
// true
// true

其他内置原型

除了Object,JS 中还存在其他内置原型:

let arr = [1, 2, 3];
console.log(arr.__proto__ === Array.prototype);
// true
​
let func = function () { };
console.log(func.__proto__ === Function.prototype);
// true

这些原型的原型指向Object.prototype。

基本类型

对于基本类型(字符串、number、boolean),它们不是对象,但是依然存在原型,在调用这些类型的方法时,JS 会临时使用原型创建对象,并调用这些原型对象的方法,然后销毁原型对象。

因此String.prototype、Number.prototype和Boolean.prototype依然可以使用。

更改原生原型

可以通过修改原生原型的方式为所有同一类型的变量添加新功能:

String.prototype.repeat = function (times) {
    let result = '';
    for (let i = 0; i < times; i++) {
        result += this;
    }
    return result;
}
​
console.log('a'.repeat(5));
// aaaaa

虽然这么做有效,但一般不建议这么做,因为这很容易出现冲突,比如 JS 库 A 为String.prototype添加了一个show方法,JS 库 B 也为String.prototype 添加了一个show 方法,两者就会冲突,最终其中一个会覆盖另一个。

唯一建议这么做的情景是,如果某个 JS 新特性部分浏览器还不支持,可以通过这种方式添加,比如:

if (!String.prototype.repeat) { // if there's no such method
  // add it to the prototype
​
  String.prototype.repeat = function(n) {
    // repeat the string n times
​
    // actually, the code should be a little bit more complex than that
    // (the full algorithm is in the specification)
    // but even an imperfect polyfill is often considered good enough
    return new Array(n + 1).join(this);
  };
}
​
alert( "La".repeat(3) ); // LaLaLa

从原型中借用

可以从已有的原型中借用一些有用的方法,比如:

let obj3 = {
    0: 'hello',
    1: 'world',
    length: 2
}
​
obj3.join = Array.prototype.join;
console.log(obj3.join(','));
// hello,world

这里利用Array.prototype中的join方法为obj3对象添加了一个join方法,因为数组的join方法本身只要求接收的参数是一个类数组(数字索引 + length 属性)。

当然,如果有必要,可以将obj3的原型设置为Array.prototype,这样obj3就可以使用所有的数组方法。

No __proto__

现代 JS 规范中不推荐使用__prototype__直接获取/修改原型。更现代的方式:

let obj4 = {
    0: 'hello',
    1: 'world',
    length: 2
};
​
console.log(Object.getPrototypeOf(obj4) === Object.prototype);
Object.setPrototypeOf(obj4, Array.prototype);
console.log(obj4.join(','));
// hello,world

除了getPrototypeOf和setPrototypeOf外,还有一个create方法,可以创建一个使用了指定原型对象的新对象:

let obj5 = Object.create(Array.prototype);
obj5.push('hello', 'world');
console.log(obj5.join(','));
// hello,world

__proto__仅应该在定义对象的时候指定原型时使用:

let obj6 = {
    0: 'hello',
    1: 'world',
    length: 2,
    __proto__: Array.prototype
}
console.log(obj6.join(','));

不过同样也可以用create方法取代,因为create方法可以接收一个属性描述符参数:

let obj7 = Object.create(Array.prototype,{
    0: {
        value: 'hello'
    },
    1: {
        value: 'world'
    },
    length: {
        value: 2
    }
});
console.log(obj7.join(','));

The End.

本文的完整示例代码可以从这里获取。

参考资料

  • 现代 JavaScript 教程

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: 暂无
最后更新:2026年4月7日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇

文章评论

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回复

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号