JavaScript prototype特性的底层机制
JavaScript prototype特性的底层机制
一、JavaScript中的对象与原型
在JavaScript中,对象是一种复合数据类型,它可以包含零个或多个键值对。每个对象都有一个与之关联的原型对象。原型对象本身也是一个对象,它为其关联的对象提供了属性和方法的共享机制。
(一)创建对象与原型关联
- 字面量方式创建对象
通过字面量方式创建的对象,其原型指向
Object.prototype
。例如:
let obj = {name: 'John'};
console.log(Object.getPrototypeOf(obj) === Object.prototype); // true
这里,Object.getPrototypeOf
方法用于获取对象的原型。从代码可以看出,通过字面量创建的 obj
对象,其原型就是 Object.prototype
。
- 构造函数方式创建对象
当使用构造函数创建对象时,新创建的对象的原型会指向构造函数的
prototype
属性。比如:
function Person(name) {
this.name = name;
}
let person = new Person('Jane');
console.log(Object.getPrototypeOf(person) === Person.prototype); // true
在上述代码中,Person
是一个构造函数,使用 new
关键字创建的 person
对象,其原型指向 Person.prototype
。
二、Prototype 链的形成
原型链是JavaScript实现继承和属性查找的重要机制。当访问一个对象的属性或方法时,如果该对象本身没有这个属性或方法,JavaScript会沿着原型链向上查找,直到找到该属性或方法,或者到达原型链的顶端(null
)。
(一)原型链示例
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
console.log(this.name +'makes a sound.');
};
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
let myDog = new Dog('Buddy', 'Golden Retriever');
// 查找 speak 方法
myDog.speak(); // Buddy makes a sound.
在这个例子中,myDog
对象首先在自身查找 speak
方法,没有找到。然后沿着原型链向上,在 Dog.prototype
中没有找到,继续向上在 Animal.prototype
中找到了 speak
方法并执行。
(二)原型链的顶端
原型链的顶端是 null
。当在原型链上查找属性或方法,一直到顶端都没有找到时,返回 undefined
。例如:
let emptyObj = {};
console.log(emptyObj.someNonexistentMethod); // undefined
这里 emptyObj
对象沿着原型链查找 someNonexistentMethod
,一直到 Object.prototype
的原型(null
)都未找到,所以返回 undefined
。
三、Prototype特性的底层存储与实现
在JavaScript引擎内部,对象的原型关系是通过内部属性 [[Prototype]]
来实现的。这个内部属性在JavaScript代码中不能直接访问,但可以通过 Object.getPrototypeOf
和 Object.setPrototypeOf
方法间接操作。
(一)[[Prototype]]
的存储
不同的JavaScript引擎可能有不同的方式来存储 [[Prototype]]
。在V8引擎中,对象在内存中以一种称为“隐藏类(Hidden Class)”的数据结构来表示。每个对象都有一个指向其隐藏类的指针,隐藏类记录了对象的布局信息,包括属性的偏移量等。而 [[Prototype]]
则是对象结构的一部分。
(二)属性查找与 [[Prototype]]
当进行属性查找时,JavaScript引擎首先在对象自身的属性列表中查找。如果没有找到,就会通过 [[Prototype]]
指针找到原型对象,然后在原型对象的属性列表中查找,依此类推沿着原型链进行查找。例如:
function Parent() {
this.parentProp = 'parent value';
}
function Child() {
this.childProp = 'child value';
}
Child.prototype = Object.create(Parent.prototype);
let childObj = new Child();
// 查找 parentProp 属性
console.log(childObj.parentProp); // parent value
在这个例子中,childObj
对象自身没有 parentProp
属性,引擎通过 [[Prototype]]
找到 Child.prototype
,再通过 Child.prototype
的 [[Prototype]]
找到 Parent.prototype
,从而找到了 parentProp
属性。
四、Prototype与构造函数的关系
构造函数的 prototype
属性是一个对象,这个对象将成为通过该构造函数创建的所有对象的原型。
(一)构造函数的 prototype 属性
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.getArea = function() {
return Math.PI * this.radius * this.radius;
};
let circle = new Circle(5);
console.log(circle.getArea()); // 78.53981633974483
在上述代码中,Circle
构造函数的 prototype
属性上定义了 getArea
方法。通过 Circle
构造函数创建的 circle
对象可以访问到这个方法,因为 circle
的原型指向 Circle.prototype
。
(二)constructor 属性
在每个构造函数的 prototype
对象上,都有一个 constructor
属性,它指向该构造函数本身。例如:
function Square(side) {
this.side = side;
}
console.log(Square.prototype.constructor === Square); // true
这个 constructor
属性在需要知道对象是由哪个构造函数创建时很有用。但需要注意的是,当手动修改原型对象时,可能需要手动修正 constructor
属性。例如:
function Rectangle(width, height) {
this.width = width;
this.height = height;
}
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
function Square2(side) {
Rectangle.call(this, side, side);
}
Square2.prototype = Object.create(Rectangle.prototype);
// 修正 constructor 属性
Square2.prototype.constructor = Square2;
let square = new Square2(5);
console.log(square.constructor === Square2); // true
如果不修正 constructor
属性,square.constructor
将指向 Rectangle
,这可能会导致一些混淆。
五、Prototype在继承中的应用
JavaScript通过原型链实现继承,这使得代码可以复用已有对象的属性和方法。
(一)原型式继承
let personPrototype = {
name: 'Default Name',
greet: function() {
console.log('Hello, I\'m'+ this.name);
}
};
let person1 = Object.create(personPrototype);
person1.name = 'John';
person1.greet(); // Hello, I'm John
在这个例子中,person1
通过 Object.create
方法以 personPrototype
为原型创建。person1
可以访问 personPrototype
的 greet
方法,并且有自己的 name
属性。
(二)经典继承模式(构造函数 + 原型链)
function Shape(color) {
this.color = color;
}
Shape.prototype.getColor = function() {
return this.color;
};
function Rectangle(width, height, color) {
Shape.call(this, color);
this.width = width;
this.height = height;
}
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;
Rectangle.prototype.getArea = function() {
return this.width * this.height;
};
let rect = new Rectangle(5, 10,'red');
console.log(rect.getColor()); // red
console.log(rect.getArea()); // 50
这里 Rectangle
继承自 Shape
,通过构造函数 Shape.call(this, color)
继承了 Shape
的属性,通过原型链 Rectangle.prototype = Object.create(Shape.prototype)
继承了 Shape
的方法。
六、Prototype的动态性
JavaScript中对象的原型是动态的,可以在运行时进行修改。
(一)修改现有对象的原型
function Vehicle() {
this.wheels = 4;
}
Vehicle.prototype.move = function() {
console.log('Moving with'+ this.wheels +'wheels.');
};
function Bicycle() {
this.wheels = 2;
}
let bike = new Bicycle();
// 动态修改 bike 的原型
Object.setPrototypeOf(bike, Vehicle.prototype);
bike.move(); // Moving with 2 wheels.
在上述代码中,bike
原本是 Bicycle
的实例,通过 Object.setPrototypeOf
方法将其原型修改为 Vehicle.prototype
,从而可以调用 Vehicle.prototype
上的 move
方法。
(二)对原型的修改影响所有相关对象
当对原型对象进行修改时,所有以该原型为基础的对象都会受到影响。例如:
function Animal2() {}
Animal2.prototype.speak = function() {
console.log('I\'m an animal.');
};
let cat = new Animal2();
let dog = new Animal2();
Animal2.prototype.speak = function() {
console.log('I\'m a different animal.');
};
cat.speak(); // I'm a different animal.
dog.speak(); // I'm a different animal.
这里对 Animal2.prototype
的 speak
方法进行了修改,cat
和 dog
作为以 Animal2.prototype
为原型的对象,都受到了影响。
七、Prototype与性能
原型链的查找机制虽然强大,但也可能会带来性能问题,尤其是在原型链较长或者频繁进行属性查找时。
(一)原型链长度与查找性能
原型链越长,属性查找所需的时间就越长。因为每次查找都需要沿着原型链一级一级向上查找。例如:
function A() {}
function B() {}
function C() {}
function D() {}
B.prototype = Object.create(A.prototype);
C.prototype = Object.create(B.prototype);
D.prototype = Object.create(C.prototype);
let d = new D();
// 查找一个属性,假设在 A.prototype 上定义
console.time('lookup');
d.someProp;
console.timeEnd('lookup');
在这个例子中,如果 someProp
定义在 A.prototype
上,从 d
对象查找该属性需要经过较长的原型链,性能相对较差。
(二)避免不必要的原型链查找
为了提高性能,可以尽量避免在原型链上进行深层次的查找。一种方法是在对象自身定义常用的属性和方法,而不是依赖原型链。例如:
function User(name) {
this.name = name;
// 直接在对象自身定义方法,避免原型链查找
this.greet = function() {
console.log('Hello, I\'m'+ this.name);
};
}
let user = new User('Alice');
user.greet();
这样,调用 user.greet()
时就不需要进行原型链查找,提高了性能。
八、Prototype相关的常见问题与误解
在使用JavaScript的 prototype
特性时,有一些常见的问题和误解需要注意。
(一)this
在原型方法中的指向
在原型方法中,this
指向调用该方法的对象。例如:
function Counter() {
this.count = 0;
}
Counter.prototype.increment = function() {
this.count++;
console.log(this.count);
};
let counter = new Counter();
counter.increment(); // 1
这里 increment
方法中的 this
指向 counter
对象,所以可以正确地修改 count
属性。但如果不小心改变了 this
的指向,就可能导致错误。例如:
function Logger() {
this.message = 'Default message';
}
Logger.prototype.log = function() {
console.log(this.message);
};
let logger = new Logger();
let logFunction = logger.log;
// 这里 logFunction 中的 this 不再指向 logger
logFunction(); // undefined
在这个例子中,将 logger.log
赋值给 logFunction
后,logFunction
中的 this
不再指向 logger
,而是指向全局对象(在严格模式下为 undefined
),导致 this.message
为 undefined
。
(二)原型对象的共享性
由于原型对象是共享的,对原型对象属性的修改可能会影响到所有相关对象。例如:
function Person2() {}
Person2.prototype.hobbies = [];
let personA = new Person2();
let personB = new Person2();
personA.hobbies.push('reading');
console.log(personB.hobbies); // ['reading']
这里 personA
和 personB
共享 Person2.prototype.hobbies
数组,所以 personA
对 hobbies
的修改也影响到了 personB
。如果不希望这种共享,可以在构造函数中初始化属性,而不是在原型上。例如:
function Person3() {
this.hobbies = [];
}
let personC = new Person3();
let personD = new Person3();
personC.hobbies.push('swimming');
console.log(personD.hobbies); // []
这样每个对象都有自己独立的 hobbies
数组。
九、Prototype与ES6 Classes
ES6引入了 class
语法,它在语法上更简洁,并且看起来更像传统的面向对象编程语言。但实际上,class
只是基于原型的继承的语法糖。
(一)Class 语法中的原型
class Shape2 {
constructor(color) {
this.color = color;
}
getColor() {
return this.color;
}
}
class Rectangle2 extends Shape2 {
constructor(width, height, color) {
super(color);
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
let rect2 = new Rectangle2(4, 6, 'blue');
console.log(rect2.getColor()); // blue
console.log(rect2.getArea()); // 24
在这个例子中,Rectangle2
继承自 Shape2
。虽然使用了 class
语法,但内部仍然是通过原型链实现继承。Rectangle2.prototype
仍然是一个对象,并且其 [[Prototype]]
指向 Shape2.prototype
。
(二)Class 与传统原型继承的对比
class
语法更符合人类的思维习惯,代码更易读。但从底层实现来看,和传统的原型继承没有本质区别。例如,传统的原型继承方式:
function Shape3(color) {
this.color = color;
}
Shape3.prototype.getColor = function() {
return this.color;
};
function Rectangle3(width, height, color) {
Shape3.call(this, color);
this.width = width;
this.height = height;
}
Rectangle3.prototype = Object.create(Shape3.prototype);
Rectangle3.prototype.constructor = Rectangle3;
Rectangle3.prototype.getArea = function() {
return this.width * this.height;
};
let rect3 = new Rectangle3(3, 5, 'green');
console.log(rect3.getColor()); // green
console.log(rect3.getArea()); // 15
对比这两种方式,可以发现它们实现的功能是一样的,只是 class
语法更加简洁明了。
十、Prototype特性在实际项目中的应用场景
在实际的JavaScript项目中,prototype
特性有很多应用场景。
(一)代码复用
通过原型链继承,可以复用已有对象的属性和方法。例如,在一个游戏开发项目中,可能有一个 GameObject
基类,包含一些通用的属性和方法,如位置、大小、渲染方法等。然后不同的具体游戏对象,如 Player
、Enemy
等可以继承自 GameObject
,复用这些属性和方法,减少代码冗余。
class GameObject {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
render() {
console.log('Rendering game object at (' + this.x + ','+ this.y + ')');
}
}
class Player extends GameObject {
constructor(x, y, width, height, name) {
super(x, y, width, height);
this.name = name;
}
move() {
console.log(this.name +'is moving.');
}
}
let player = new Player(10, 10, 50, 50, 'John');
player.render(); // Rendering game object at (10, 10)
player.move(); // John is moving.
(二)插件开发
在开发JavaScript插件时,可以利用原型特性来提供通用的功能。例如,开发一个DOM操作插件,可能有一个基础的 DOMPlugin
类,定义一些基本的DOM操作方法,如选择元素、添加样式等。然后不同的具体插件功能可以继承自 DOMPlugin
,扩展其功能。
class DOMPlugin {
constructor() {}
selectElement(selector) {
return document.querySelectorAll(selector);
}
addClass(element, className) {
element.classList.add(className);
}
}
class ImagePlugin extends DOMPlugin {
constructor() {
super();
}
loadImage(src) {
let img = new Image();
img.src = src;
return img;
}
}
let imagePlugin = new ImagePlugin();
let images = imagePlugin.selectElement('img');
let newImage = imagePlugin.loadImage('example.jpg');
通过深入理解JavaScript prototype
特性的底层机制,可以更好地编写高效、可维护的JavaScript代码,在各种项目中灵活运用这一特性来实现代码复用、继承等功能。