JavaScript面向对象核心概念与原型机制详解

作者:异步漫步时间:2026-10-24 04:38:10浏览:311

本文系统梳理JavaScript面向对象编程的核心概念,涵盖抽象、封装与继承三大特性。深入解析构造函数的工作原理、作用域与词法作用域规则,以及闭包的机制与性能影响。重点阐述原型与原型链的查找机制,对比ES6 class语法糖与传统原型继承的区别,并总结函数调用的四种模式及继承实现方式。

本文介绍了JavaScript面向对象编程的核心知识与关键概念,旨在帮助开发者深入理解其内部机制,供参考学习。

一、面向对象基础

1.1 概念定义

  1. 面向对象开发是指利用对象进行软件构建的过程。
  2. 面向过程侧重于按步骤执行流程,而面向对象则是对面向过程的一种封装与抽象。

1.2 三大核心特性

抽象性:在构建描述数据的对象时,需提取该对象的核心数据特征。

  1. 明确所需的核心属性与行为方法。
  2. 脱离特定上下文环境,对象的具体含义难以界定。

封装性:对象将数据与功能逻辑组合在一起,实现封装效果。

  1. 在JavaScript中,对象本质上是键值对的集合。若键对应的值是数据(包括基本类型、引用类型或null),则称为属性;若键对应的值是函数,则称为方法。
  2. 对象实现了属性与方法的封装。
  3. 方法是对具体处理过程的封装。

继承性:继承是指子类获取父类已有的成员,是实现代码复用的重要手段。

  • 在Java等强类型语言中,继承遵循class规则,class定义了对象应具备的属性和方法。
  • 这类语言中,继承发生在class之间,一个class继承另一个class后,该class便拥有了被继承class的成员,由此创建的对象也同时具备两者的成员。

JavaScript中没有严格的class继承语法(尽管ES6引入了class extend语法),通常遵循继承理念,通过扩充对象成员来实现继承,因此JS中的继承实现方式多样。

传统面向对象语言基于类进行继承,而JavaScript的继承基于对象。

一种简单的继承模式是混入(mix)。


function mix ( o1, o2 ) {
 for ( var k in o2 ) {
  o1[ k ] = o2[ k ];
 }
}

1.3 其他相关概念

类(class):在JavaScript中,构造函数即扮演了类的角色。

  • 在传统面向对象语言中,使用类定义模板,再通过模板创建对象。
  • 构造函数具有类似功能,因此常被称为类。

实例(instance)与对象(object)的关系。

  • 实例通常指由某个构造函数创建出来的对象,可称为该构造函数的实例。
  • 实例本质上就是对象,而“对象”是一个更泛化的称呼。
  • 实例与对象在概念上互为近义词。

键值对与属性、方法的对应关系。

  • 在JavaScript中,键值对的集合被称为对象。
  • 若值为非函数数据,则该键值对称为属性。
  • 若值为函数,则该键值对称为方法(method)。

父类与子类(基类与派生类)的概念。

  • 在传统面向对象语言中,通过类实现继承时,存在父类与子类的概念。
  • 父类也称为基类,子类也称为派生类。
  • JavaScript中没有类的概念,通常称为父对象、子对象、基对象或派生对象。

二、构造函数详解

2.1 构造函数的作用

  1. 主要用于初始化数据。
  2. 在JavaScript中,用于给对象添加属性并初始化属性值。

2.2 对象的创建过程

  1. 代码示例:var p = new Person();
  2. 首先,new运算符创建了一个对象,类似于{},这是一个没有任何(自定义)成员的空对象。

    • 使用new创建对象时,对象的类型即为创建它的构造函数名。
    • 使用{}无论如何结果都是Object类型,相当于new Object。
  3. 随后调用构造函数,为其初始化成员。

    • 构造函数在调用伊始,会执行赋值操作,即this = 刚刚创建出来的对象。
    • 因此,在构造函数内部,this指向刚刚创建出来的对象。
  4. 在构造函数中,利用对象的动态特性为其添加成员。

三、作用域机制

3.1 作用域定义

域表示范围,即作用域,指一个名字在何处可用、何处不可用。简单来说,作用域针对变量,例如创建一个函数a1,函数内部又包含一个子函数a2。


// 全局作用域
functiona a1() {
 // a1作用域
 function a2() {
  // a2作用域
 }
}

此时存在三个作用域:全局作用域、a1作用域、a2作用域;即全局作用域包含了a1的作用域,a2的作用域包含了a1的作用域。

当a2查找变量时,会先从自身作用域区查找,找不到则向上级a1的作用域查找,若仍未找到,则到全局作用域区查找,从而形成作用域链。

3.2 JavaScript中的词法作用域规则

  1. 函数可以访问函数外部的数据。
  2. 整个代码结构中,只有函数可以限定作用域。
  3. 作用规则首先依据提升规则进行分析。
  4. 若当前作用域中已存在同名变量,则忽略外部同名变量。

3.3 属性搜索原则

  • 属性搜索原则指对象在访问属性或方法时,首先在当前对象中查找。
  • 若当前对象中存储着该属性或方法,则停止查找并直接使用。
  • 若当前对象没有该成员,则在其原型对象中查找。
  • 若原型对象中含有该成员,则停止查找并直接使用。
  • 若原型中仍没有,则继续向原型的原型中查找。
  • 如此往复,直到为(即Object.prototype),若仍未找到则返回undefined。
  • 若是调用方法而未找到,则报错,提示该xxx不是一个函数。

四、闭包机制

4.1 对闭包的理解

使用闭包主要是为了设计私有方法和变量。闭包的优点是可以避免全局变量污染;缺点是闭包会常驻内存,增加内存使用量,使用不当易造成内存泄露。在JavaScript中,函数即闭包,只有函数才能产生作用域。

闭包具备3个特性:

  1. 函数嵌套函数。
  2. 在函数内部可以引用外部的参数和变量。
  3. 参数和变量不会以垃圾回收机制回收。

4.2 闭包的用途(特性)

闭包的作用是保存私有变量,通过提供的接口(方法)供外部使用,但外部不能直接访问该变量。使用闭包可以模拟面向对象代码风格,更优雅简洁地表达代码,并在某些方面提升执行效率。利用闭包可实现如下需求:

  1. 匿名自执行函数。

一个匿名函数立即执行,由于外部无法引用其内部变量,执行完后很快被释放,关键在于这种机制不会污染全局对象。

  1. 缓存。

闭包能做到这一点,因为它不会释放外部引用,从而函数内部的值得以保留。

  1. 实现封装。
  2. 模拟面向对象的代码风格。

4.3 闭包的基本模型

对象模式:函数内部定义一个对象,对象中绑定多个函数(方法),返回对象,利用对象的方法访问函数内的数据。


function createPerson() {
 var __name__ = "";
 return {
  getName: function () {
   return __name__;
  },
  setName: function( value ) {
   // 如果不姓张就报错
   if ( value.charAt(0) === '张' ) {
    __name__ = value;
   } else {
    throw new Error( '姓氏不对,不能取名' );
   }
  }
 }
}
var p = createPerson();
p.set_Name( '张三丰' );
console.log( p.get_Name() );
p.set_Name( '张王富贵' );
console.log( p.get_Name() );

函数模式:函数内部定义一个新函数,返回新函数,用新函数获取函数内的数据。


function foo() {
 var num = Math.random();
 function func() {
  return mun;
 }
 return func;
}
var f = foo();
// f 可以直接访问这个 num
var res1 = f();
var res2 = f();

沙箱模式:沙箱模式是一个自调用函数,代码写到函数中一样会执行,但不会与外界产生任何影响,例如jQuery。


(function () {
 var jQuery = function () { // 所有的算法 }
 // .... // .... jQuery.each = function () {}
 window.jQuery = window.$ = jQuery;
})();
$.each( ... )

4.4 闭包的性能问题

JavaScript的垃圾回收机制规定,当一个函数执行完后,其作用域会被收回;如果形成了闭包,执行完后其作用域就不会被收回。

函数执行需要内存,函数中定义的变量会在函数执行结束后自动回收。凡是因为闭包结构被引出的数据,若还有变量引用这些数据,则这些数据不会被回收。因此在使用闭包时,如果不使用某些数据了,一定要赋值一个null。


var f = (function () {
 var num = 123;
 return function () {
  return num;
 };
})();
// f 引用着函数,函数引用着变量num
// 因此在不使用该数据的时候,最好写上
f = null;

五、原型机制

5.1 什么是原型

一句话说明原型:原型能存储我们的方法,构造函数创建出来的实例对象能够引用原型中的方法。

JavaScript中一切皆对象,而每个对象都有一个原型(Object除外),这个原型大概就像Java中的父类,所以,基本上你可以认为原型就是这个对象的父对象,即每一个对象(Object除外)内部都保存了它自己的父对象,这个父对象就是原型。一般创建的对象如果没有特别指定原型,那么它的原型就是Object(这就很类似Java中所有的类默认继承自Object类)。

ES6通过引入class、extends等关键字,以一种语法糖的形式把构造函数包装成类的概念,更便于大家理解。是希望开发者不再花精力去关注原型以及原型链,也充分说明原型的设计意图和类是一样的。

5.2 查看对象的原型

当对象被创建之后,查看它们的原型的方法不止一种,以前一般使用对象的__proto__属性,ES6推出后,推荐用Object.getPrototypeOf()方法来获取对象的原型。


function A(){
 this.name='lala';
}
var a=new A();
console.log(a.__proto__) 
//输出:Object {}
 
//推荐使用这种方式获取对象的原型
console.log(Object.getPrototypeOf(a)) 
//输出:Object {}

无论对象是如何创建的,默认原型都是Object,在这里需要提及的比较特殊的一点就是,通过构造函数来创建对象,函数A本身也是一个对象,而A有两个指向表示原型的属性,分别是__proto__和prototype,而且两个属性并不相同。


function A(){
 this.name='lala';
}
var a=new A();
console.log(A.prototype) 
//输出:Object {}
 
console.log(A.__proto__) 
//输出:function () {}
console.log(Object.getPrototypeOf(A))
//输出:function () {}

函数的prototype属性只有在当作构造函数创建的时候,把自身的prototype属性值赋给对象的原型。而实际上,作为函数本身,它的原型应该是function对象,然后function对象的原型才是Object。

总之,建议使用ES6推荐的查看原型和设置原型的方法。

5.3 原型的用法

其实原型和类的继承的用法是一致的:当你想用某个对象的属性时,将当前对象的原型指向该对象,你就拥有了该对象的使用权了。


function A(){
 this.name='world ';
}
function B(){
 this.bb="hello"
}
var a=new A();
var b=new B();

//将b设置为a的原型,此处有一个问题,即a的constructor也指向了B构造函数,可能需要纠正 
Object.setPrototypeOf(a,b);
a.constructor=A;
console.log(a.bb); //hello

如果使用ES6来做的话则简单许多,甚至不涉及到prototype这个属性。


class B{
  constructor(){
   this.bb='hello'
  }
}
class A extends B{
  constructor(){
  super();
  this.name='world';
  }
}
 
var a=new A();
console.log(a.bb+" "+a.name); //hello world
console.log(typeof(A))    //"function"

怎么样?是不是已经完全看不到原型的影子了?活脱脱就是类继承,但是你也看得到实际上类A的类型是function,所以说,本质上class在JS中是一种语法糖,JS继承的本质依然是原型,不过,ES6引入class、extends来掩盖原型的概念也是一个很友好的举动,对于长期学习那些类继承为基础的面向对象编程语言的程序员而言。

我的建议是,尽可能理解原型,尽可能用class这种语法糖。

好了,问自己两个问题:

  1. 为什么要使用原型?——提高函数的复用性。
  2. 为什么属性不放在原型上而方法要放在原型上?

    • 利用对象的动态特性:构造函数.prototype.xxxx = vvv
    • 利用直接替换。
    
    Student.prototype = {
    sayHello : function(){},
    study : function(){}
    };

5.4 原型链

什么是原型链?凡是对象就有原型,那么原型又是对象,因此凡是给定一个对象,那么就可以找到他的原型,原型还有原型,那么如此下去,就构成一个对象的序列,称该结构为原型链。

每个实例对象都有一个__proto_属性,该属性指向它原型对象,这个实例对象的构造函数有一个原型属性prototype,与实例的__proto__属性指向同一个对象。当一个对象在查找一个属性的时,自身没有就会根据__proto__ 向它的原型进行查找,如果都没有,则向它的原型的原型继续查找,直到查到Object.prototype._proto_为null,这样也就形成了原型链。

这个概念其实也变得比较简单,可以类比类的继承链条,即每个对象的原型往上追溯,一直到Object为止,这组成了一个链条,将其中的对象串联起来,当查找当前对象的属性时,如果没找到,就会沿着这个链条去查找,一直到Object,如果还没发现,就会报undefined。

原型链的结构:凡是使用构造函数,创建出对象,并且没有利用赋值的方式修改原型,就说该对象保留默认的原型链。


function Person(){}
var p = new Person();
//p 具有默认的原型链

默认的原型链结构就是:当前对象 -> 构造函数.prototype -> Object.prototype -> null

在实现继承的时候,有时候会利用替换原型链结构的方式实现原型继承,那么原型链结构就会发生改变。


function DunizbCollection(){}
DunizbCollection.prototype = [];
var arr = new DunizbCollection();

此时arr对象的原型链结构被指向了数组对象的原型链结构了:arr -> [] -> Array.prototype -> Object.prototype -> null

用图形表示对象的原型链结构:以如下代码为例绘制原型链结构。


function Person(){}
var p = new Person();

原型链结构图为:一、面向对象基础

使用原型需要注意两点:

  1. 原型继承链条不要太长,否则会出现效率问题。
  2. 指定原型时,注意constructor也会改变。

六、继承实现

实现继承有两种常见方式:

6.1 混合式继承

最简单的继承就是将别的对象的属性强加到我身上,那么我就有这个成员了。


function Person() {};
Person.prototype.extend = function ( o ) {
  for ( var k in o ) {
   this[ k ] = o[ k ];
  }
};
Person.prototype.extend({
  run: function () { console.log( '我能跑了' ); },
  eat: function () { console.log( '我可以吃了' ); },
  sayHello: function () { console.log( '我吃饱了' ); }
});

6.2 原型继承

利用原型也可以实现继承,不需要在我身上添加任何成员,只要原型有了我就有了。

6.3 借用构造函数继承

这种技术的基本思想相当简单,即在子类型构造函数的内部调用超类型构造函数,而函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法也可以在(将来)新创建的对象上执行构造函数。


function Person ( name, age, gender ) {
  this.name = name;
  this.age = age;
  this.gender = gender;
}
// 需要提供一个 Student 的构造函数创建学生对象
// 学生也应该有 name, age, gender, 同时还需要有 course 课程
function Student ( name, age, gender, course ) {
  Person.call( this, name, age, gender );
  this.course = course;
}

在《JavaScript高级程序设计(第三版)》中详细介绍了继承的6种方式。

七、函数的四种调用模式

7.1 函数模式

就是一个简单的函数调用。函数名的前面没有任何引导内容。


function foo () {}
var func = function () {};
...
foo();
func();
(function () {} )();

this的含义:在函数中this表示全局对象,在浏览器中是window。

7.2 方法模式

方法一定依附于一个对象,将函数赋值给对象的一个属性,那么就成为了方法。


function f() {
 this.method = function () {};
}
var o = {
 method: function () {}
}

this的含义:这个依附的对象。

7.3 构造器调用模式

创建对象的时候构造函数做了什么?由于构造函数只是给this添加成员,没有做其他事情。而方法也可以完成这个操作,就是this而言,构造函数与方法没有本质的区别。

特征:

  1. 使用new关键字,来引导构造函数。
  2. 构造函数中的this与方法中的一样,表示对象,但是构造函数中的对象是刚刚创建出来的对象。
  3. 构造函数中不需要return,就会默认的return this。

    • 如果手动添加return,就相当于return this。
    • 如果手动的添加return基本类型,无效,还是保留原来返回this。
    • 如果手动添加的return null,或return undefined,无效。
    • 如果手动添加return对象类型,那么原来创建的this就会被丢掉,返回的是return后面的对象。

7.4 上下文调用模式

上下文就是环境。就是自己定义设置this的含义。

语法:

  1. 函数名.apply( 对象, [ 参数 ] );
  2. 函数名.call( 对象, 参数 );

描述:

  1. 函数名就是表示函数本身,使用函数进行调用的时候默认this是全局变量。
  2. 函数名也可以是方法提供,使用方法调用的时候,this是指向当前对象。
  3. 使用apply进行调用后,无论是函数还是方法都无效了,我们的this,由apply的第一个参数决定。

参数问题:无论是call还是apply在没有后面的参数的情况下(函数无参数,方法无参数)是完全一致的。


function foo(){
 console.log( this );
}
foo.apply( obj );
foo.call( obj );

第一个参数的使用也是有规则的:

  • 如果传入的是一个对象,那么就相当于设置该函数中的this为参数。
  • 如果不传入参数,或传入null、undefined等,那么相当于this默认为window。

foo();
foo.apply();
foo.apply( null );
  • 如果传入的是基本类型,那么this就是基本类型对应的包装类型的引用。

在使用上下文调用的时候,原函数(方法)可能会带有参数,那么这个参数再上下文调用中使用第二个(第n个)参数来表示。


function foo( num ) {
 console.log( num );
}
foo.apply( null, [ 123 ] );
// 等价于
foo( 123 );

参考资料

  • 本文原型部分部分引用自《JavaScript原型详解》,版权归原作者所有。
  • js闭包的用途

感兴趣的朋友可以使用在线HTML/CSS/JavaScript代码运行工具:http://tools.jb51.net/code/HtmlJsRun测试上述代码运行效果。

更多关于JavaScript相关内容感兴趣的读者可查看本站专题:《javascript面向对象入门教程》、《JavaScript错误与调试技巧总结》、《JavaScript数据结构与算法技巧总结》、《JavaScript遍历算法与技巧总结》及《JavaScript数学运算用法总结》

希望本文所述对大家JavaScript程序设计有所帮助。

作者最新文章