本文系统回顾JavaScript面向对象编程基础,涵盖对象本质、五种创建方式(字面量、构造函数、工厂模式等)、this在不同执行环境下的指向规则,以及原型与原型链机制。深入解析如何利用原型实现继承及潜在问题,帮助开发者夯实JS核心基础。
引言
原计划继续撰写由浅入深的表达式系列最后一篇,但近期团队工作异常繁忙,这种高强度状态前所未有。不过,喜爱表达式系列的朋友请放心,相关内容正在撰写中。在实际工作中,我发现团队对JavaScript的一些基本原理存在理解模糊的情况,因此决定先花时间整理这些基础知识与大家分享,后续还会附上培训用的PPT。起初计划只写一篇文章,但随着内容扩展,决定将其整理为一个系列。本系列聚焦JavaScript基础,虽不涉及前沿技术,但相信这些基础知识能帮助你更好地理解后续有趣的内容。
- Javascript基础回顾之(一) 类型
- Javascript基础回顾 (二) 作用域
- Javascript基础回顾之(三) 面向对象
JavaScript面向对象核心内容概览
本篇是“你必须知道的Javascript”系列的第三篇,主要探讨JavaScript如何实现面向对象编程。核心内容涵盖以下方面:
- Javascript中的对象
- 什么是对象
- 遍历属性
- 创建对象
- 工厂模式
- 构造函数模式
- 详解this
- 在函数中
- 在对象方法中
- 在构造函数中
- 在call和apply中
- 在bind中
- 在dom元素事件处理函数中
- 详解prototype
- 什么是原型
- 什么是原型链
- 利用原型链实现继承
- 原型链中的问题
JavaScript对象基础
Javascript中的对象
什么是对象
在JavaScript中,我们可以将对象理解为一组无序的键值对,类似于C#中的Dictionary<string,Object>。其中Key代表属性名称,而Value可以是以下三种类型之一:
- 基本值(string, number, boolean, null, undefined)
- 对象
- 函数
var o = new Object();
o["name"] = "jesse"; //基本值作为对象属性
o["location"] = { //对象作为对象属性
"city": "Shanghai",
"district":"minhang"
};
// 函数 作为对象属性
o["sayHello"] = function () {
alert("Hello, I am "+ this.name + " from " + this.location.city);
}
o.sayHello();
对象属性遍历
遍历属性
在C#中,我们可以使用foreach遍历Dictionary<string,Object>。既然JavaScript中的对象被视为键值对,那么如何进行遍历呢?
for (var p in o) {
alert('name:'+ p +
' type:' + typeof o[p]
);
}
// name:name type:string
// name:location type:object
// name:sayHello type:function
上述遍历方式会将原型中的属性一并包含进来。关于原型的定义以及如何区分原型属性与实例属性,将在后文详细讲解。
创建对象的多种方式
创建对象 在前文我们已经创建了对象,并使用了以下两种创建方式。
- 利用new创建一个Object的实例。
- 字面量
前文中的o是通过第一种方式创建的,而o中的location属性则是通过字面量方式创建的。第一种方式实际上也被称为构造函数模式,因为Object本质上是一个构造函数,用于生成Object的实例。如果对构造函数部分仍有疑问,建议回顾我的第一篇关于类型基础的文章,特别是Object与object的区别。
除了上述两种方式外,我们还介绍其他几种创建对象的方法:
工厂模式
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function(){
alert(this.name);
};
return o;
}
var person1 = createPerson('Jesse', 29, 'Software Engineer');
var person2 = createPerson('Carol', 27, 'Designer');
这种模式创建的对象存在一个问题:它在函数内部创建了一个Object实例,该实例与构造函数createPerson本身没有直接关联。

由于在内部使用new Object()创建对象,因此它是Object的实例。这意味着我们无法通过实例判断它具体是由哪个函数创建的。
构造函数模式详解
构造函数模式
工厂模式未能解决对象识别问题。我们可以思考,Object()本质上也是一个函数,当在其前加上new时,它变为构造函数并生成Object实例。同理,我们可以在其他函数前加上new,从而生成该函数的实例,这就是所谓的构造函数模式。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var p1 = new Person('Jesse', 18, 'coder');
alert(p1 instanceof Person); // true
this关键字在不同场景下的指向
详解this this在JavaScript中是一个特殊的对象,准确地说,this本身就是一个对象。在上一篇关于作用域和作用域链的文章中,我们提到了变量对象,它决定了当前执行环境中可访问的属性和函数。从某种程度上看,可以将this视为该变量对象。之前提到最大的执行环境是全局执行环境,而window即为全局执行环境中的变量对象,因此在全局环境中this===window会返回true。

除了全局执行环境外,我们还提到了另一种执行环境,即函数。每个函数都有一个this对象,但其代表的值可能不同,主要取决于函数的调用者。以下列举几种场景:
函数
function f1(){
return this;
}
f1() === window; // global object
当函数在全局环境中运行时,函数内的this对象指向全局变量对象,即window。需要注意的是,在严格模式下,这种方式会返回undefined。
对象方法
var o = {
prop: 37,
f: function() {
return this.prop;
}
};
console.log(o.f()); // logs 37
在对象方法中,this对象指向当前实例对象。注意:无论函数在哪里、何时或以何种方式定义,只要它是对象实例的方法,其this始终指向该对象实例。
var o = { prop: 37 };
var prop = 15;
function independent() {
return this.prop;
}
o.f = independent;
console.log(independent()); // logs 15
console.log(o.f()); // logs 37
区别:上述函数independent如果直接执行,this指向全局执行环境,因此this.prop指向全局变量prop。但如果将independent设为对象o的一个属性,那么independent中的this将指向该实例,同理this.prop变为对象o的prop属性。
构造函数
前文提到使用构造函数创建对象,实际上是利用了this的这种特性。在构造函数中,this对象指向该构造函数实例化出的对象。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
var p1 = new Person('Jesse', 18, 'coder');
var p2 = new Person('Carol',16,'designer');
当我们实例化Person得到p1时,this指向p1。而当实例化Person得到p2时,this指向p2。
利用call和apply
当使用call和apply调用某个函数时,该函数中的this对象会被绑定到指定的对象上。call和apply的主要区别在于,apply要求传入一个数组作为参数列表。
function add(c, d) {
return this.a + this.b + c + d;
}
var o = { a: 1, b: 3 };
// 第一个参数会被绑定成函数add的this对象
add.call(o, 5, 7); // 1 + 3 + 5 + 7 = 16
// 第二个参数是数组作为arguments传入方法add
add.apply(o, [10, 20]); // 1 + 3 + 10 + 20 = 34
在bind方法中
bind方法存在于Function.prototype.bind中,即所有function都具备此方法。当我们调用某个方法的bind时,会生成一个与原方法相同的新方法,只不过this指向bind传入的第一个参数。
function f() {
return this.a;
}
var g = f.bind({ a: "azerty" });
console.log(g()); // azerty
var o = { a: 37, f: f, g: g };
console.log(o.f(), o.g()); // 37, azerty
在dom元素事件处理器中
在事件处理函数中,this指向触发该事件的dom元素。
HTML代码
<html>
<body>
<div id="mydiv" style="width:400px; height:400px; border:1px solid red;"></div>
<script type="text/javascript" src="essence.js"></script>
</body>
</html>
JavaScript代码
function click(e) {
alert(this.nodeName);
}
var myDiv = document.getElementById("mydiv");
myDiv.addEventListener('click', click, false);
当我们点击页面中的那个div时,毫无疑问,它会显示DIV的内容。

原型(prototype)与原型链机制
详解prototype prototype即原型,也是JavaScript中一个重要的概念。在讨论原型之前,我们需要回顾之前的构造函数模式。在使用构造函数创建对象时,主要利用了this的特性。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};
}
var p1 = new Person('Jesse', 18, 'coder');
var p2 = new Person('Carol', 17, 'designer');
前文提到,当用Person实例化p1时,Person中的this指向p1;当实例化p2时,this指向p2。这意味着,虽然p1和p2中的sayName起到相同作用,但实际上它们并非同一个函数。

也就是说,它们在内存堆中存在多份拷贝,而非栈中引用地址的拷贝。这是否符合面向对象思想暂且不论,至少对内存而言是一种浪费。解决之道正是我们要讨论的原型。
什么是原型
在JavaScript中,每个函数都有一个原型对象,该原型对象与普通对象无异。只不过默认有一个constructor属性指向该函数。同时,该函数的所有实例都有一个引用指向这个原型对象。如果仍不清楚,请参考下图:

以上展示了构造函数、构造函数原型以及实例之间的关系。以Person构造函数为例,所有Person的实例(p1, p2)都通过一个prototype属性指向Person构造函数prototype对象。如此一来,我们可以将方法写在原型上,所有实例即可访问同一个方法。
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
Person.prototype.sayName = function () {
alert(this.name);
}
}
var p1 = new Person('Jesse', 18, 'coder');
var p2 = new Person('Carol', 17, 'designer');
alert(p1.sayName == p2.sayName); // true
什么是原型链
大家还记得作用域链吗?如果不记得,请回顾第二篇(作用域和作用域链)。简单来说,当在一个执行环境中访问某个变量时,如果当前环境不存在该变量,则会到其包含环境(即外层)查找,若外层仍找不到,则继续向外层查找,直到全局执行环境,这就是作用域链。原型链类似,只是场景切换到了对象实例中。如果在一个实例中查找某个属性,而该实例中没有,则会到其原型中查找。注意,前文提到原型也是一个对象,它也有自己的原型对象,因此形成链式结构。实例自己的原型中找不到,则到原型的原型对象中查找,一直向上延伸到Object的原型对象。默认情况下,我们创建的函数的原型对象的原型指向Object的原型对象,因此我们可以在自定义构造函数的实例上调用Object的方法(如toString, valueOf)。

利用原型链实现继承
利用原型实现继承
前文已讲解JavaScript中的继承,主要依靠原型链实现。所有实例继承自Object,是因为默认情况下,我们创建的所有函数的原型对象的原型都指向Object对象。同理,我们可以定义自己的继承关系。
function Person(name, age, job) {
this.name = name;
this.age = age;
}
Person.prototype.sayName = function () {
alert(this.name);
}
function Coder(language){
this.language = language;
}
Coder.prototype = new Person(); //将 Coder 的原型指向一个Person实例实现继Person
Coder.prototype.code = function () {
alert('I am a '+ this.language +' developer, Hello World!');
}
function Designer() {
}
Designer.prototype = new Person(); //将 Desiger 的原型指向一个Person实例实现继Person
Designer.prototype.design = function () {
alert('其实我只是一个抠图工而已。。。。');
}
var coder = new Coder('C#');
coder.name = 'Jesse';
coder.sayName(); //Jesse
coder.code(); // I am a C# developer, Hello World!
var designer = new Designer();
designer.name = 'Carol';
designer.sayName(); // Carol
designer.design(); // 其实我只是一个抠图工而已。。。。
原型链继承的潜在问题
原型链中的问题
由于原型对象以引用方式保存,赋值时需特别注意,否则可能覆盖之前赋的值。例如上述代码中,若先写原型方法再实现继承,则原型方法将丢失。
function Coder(language){
this.language = language;
}
Coder.prototype.code = function () {
alert('I am a '+ this.language +' developer, Hello World!');
}
Coder.prototype = new Person(); //这里会覆盖上面所有的原型属性和方法
var coder = new Coder('C#');
coder.name = 'Jesse';
coder.sayName();
coder.code(); // 这里会报错,找不到code方法。
总结
至此,三篇文章均已完成。