JavaScript原理篇——理解对象、构造函数、原型、继承

2024-05-09 16:44

本文主要是介绍JavaScript原理篇——理解对象、构造函数、原型、继承,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

对象:在JavaScript中,几乎所有的东西都是对象,包括基本数据类型的包装对象。对象是属性的集合,每个属性都有一个键和一个值。对象可以通过字面量、构造函数或Object.create()等方式创建。

构造函数:构造函数是用来创建对象的函数,通过new关键字调用构造函数可以创建对象实例。构造函数可以定义对象的属性和方法,实例化后的对象可以共享构造函数中定义的方法。

原型:每个JavaScript对象都有一个原型对象,可以通过__proto__访问。原型是对象的模板,包含对象共享的属性和方法。构造函数的prototype属性指向原型对象,实例的__proto__属性指向构造函数的原型对象。

继承:JavaScript使用原型链实现继承,子类对象可以继承父类对象的属性和方法。子类构造函数的原型对象可以设置为父类构造函数的实例,从而实现继承。继承可以通过原型链继承、构造函数继承、组合继承等方式实现。

对象基础操作

javaScript 对象(Object)是 JavaScript 中最基本的数据结构之一,有许多基础操作可以用来创建、操作和管理对象。以下是一些常见的 JavaScript 对象基础操作:

创建对象

对象字面量表示法:

字面量是创建对象最常用的表示方法

const obj = {property1: value1,property2: value2,// ...
};

使用 Object 构造函数:

const obj = new Object();
obj.property1 = value1;
obj.property2 = value2;

使用 Object.create() 方法:

const obj = Object.create(Object.prototype, {property1: {value: value1,writable: true,configurable: true,enumerable: true,},property2: {value: value2,writable: true,configurable: true,enumerable: true,},
});

访问对象属性

点表示法:

const value1 = obj.property1;

方括号表示法:

const value1 = obj['property1'];

添加/修改/删除对象属性

添加或修改对象属性:

obj.property1 = value1;
// 或
obj['property1'] = value1;

删除对象属性:

delete obj.property1;
// 或
delete obj['property1'];

检查对象属性

检查对象是否有某个属性:

if ('property1' in obj) {// 对象 obj 有 property1 属性
}

检查对象自有属性:

if (Object.prototype.hasOwnProperty.call(obj, 'property1')) {// 对象 obj 自有 property1 属性
}

枚举对象属性

使用 for...in 循环:

for (const property in obj) {if (obj.hasOwnProperty(property)) {console.log(property, obj[property]);}
}

获取对象原型

使用 Object.getPrototypeOf()

const prototype = Object.getPrototypeOf(obj);

继承和扩展对象

使用 Object.create() 创建继承对象:

const parentObj = {property1: value1,
};
const obj = Object.create(parentObj);

使用 Object.assign() 合并对象:

const obj1 = {property1: value1,
};
const obj2 = {property2: value2,
};
const obj = Object.assign({}, obj1, obj2);

使用 Object.setPrototypeOf() 设置原型:

const prototypeObj = {// ...
};
Object.setPrototypeOf(obj, prototypeObj);

对象key要求

在JavaScript中,对象的键(key)有一些基本的要求:

  1. 唯一性:每个键必须是唯一的,不能有两个相同的键存在于同一个对象中。如果有重复的键,后面的值会覆盖前面的值。

  2. 数据类型:键可以是以下几种数据类型:

    • 字符串:字符串是JavaScript中最常见的键类型,例如 {"name": "John"}
    • 符号(Symbol):从ES6开始,可以使用Symbol创建独一无二的键,例如 let obj = { [Symbol('key')]: 'value' }
    • 基本数据类型:其他基本数据类型(如数字、布尔值)会自动转换为字符串,例如 let obj = { 1: 'one', true: 'yes' },但不推荐,因为可能会与预期不符。
  3. 可枚举性:默认情况下,对象的键是可枚举的,这意味着它们在for...in循环中会被遍历到。可以通过Object.defineProperty来改变键的可枚举性。

  4. 保留字:JavaScript有一些保留字不能用作对象的键,如 class, for, function 等,如果尝试用这些词作为键,会引发错误。

  5. 空值nullundefined 也不能作为键,因为它们在JavaScript中被视为特殊的值。

基本类型的包装对象Number/Boolean/String

包装对象是 JavaScript 中的三个构造函数:Number、String 和 Boolean。这些构造函数可以用于创建对应的包装对象:

  • new Number(value) 创建一个 Number 对象,包装一个数值值。
  • new String(value) 创建一个 String 对象,包装一个字符串值。
  • new Boolean(value) 创建一个 Boolean 对象,包装一个布尔值。

在JavaScript中,基本数据类型(如字符串、数字、布尔值等)并不是对象,它们是不可变的。然而,为了能够调用一些方法和属性,JavaScript提供了基本数据类型的包装对象。这些包装对象是临时创建的对象,用于给基本数据类型值提供对象形式的方法和属性访问。

举个例子,当你使用"Hello".toUpperCase()时,JavaScript会临时将字符串"Hello"转换为一个字符串对象,然后调用toUpperCase()方法,最后丢弃这个临时对象。这样就可以在基本数据类型上调用对象的方法。

需要注意的是,虽然可以在基本数据类型上调用对象的方法,但是这些包装对象并不是基本数据类型的值本身,它们是对象。因此,在比较基本数据类型值时,最好使用严格相等运算符(===)来避免类型转换带来的意外结果。

构造函数、原型对象、实例

构造函数也是通过function方式定义的函数,内部使用this,通过new实例化后this的属性和方法执行实例本身。

原型是一个对象,原型对象依附于构造函数存在。构造函数通过.prototype访问原型对象,并通过function.prototype.方法(){}的形式添加实例所需的公共方法。

实例是通过构造函数通过new生成的,实例能够使用构造函数原型对象上所有公共的属性和方法。

通常代码逻辑是:写构造函数,为构造函数原型对象新增方法,通过new得到实例对象

构造函数VS普通函数

根据 JavaScript 的特性,函数在 JavaScript 中也是一种对象,被称为“可调用的对象”。函数对象和普通对象一样,可以拥有自己的属性和方法。在 JavaScript 中,函数可以被当作构造函数来使用,通过 new 关键字来实例化对象。当一个函数被用作构造函数时,会创建一个新的对象,该对象的原型会指向构造函数的原型对象(即构造函数的 prototype 属性)。

函数对象的 prototype 属性是一个指向原型对象的指针,它包含了一个对象的属性和方法,可以被实例化的对象所共享。通过在函数对象上设置原型对象的属性和方法,可以实现对所有实例对象的共享属性和方法。

虽然大部分函数可能不是专门设计用来作为构造函数的,但是在 JavaScript 中,任何函数都可以被当作构造函数来使用。这也是 JavaScript 中的一种灵活性和特性,使得开发者可以根据需要灵活地使用函数对象来实现不同的功能。

new关键字

new构造函数,返回一个对象,要么是构造函数本身有返回值,并且返回值是对象;要么是构造函数生成的实例对象。

构造函数没有返回值

function Person(name) {this.name = name;
}
const person = new Person("lucas");
console.log(person);

返回的是person实例对象

构造函数有返回值

function Person(name) {this.name = name;return { 1: 1 };
}
const person = new Person("lucas");
console.log(person);

构造函数如果有显式返回值,且返回值为对象类型,那么构造函数返回结果不再是目标实例。

构造函数返回的非对象类型

返回的是非对象,不影响实例的返回。

function Person(name) {this.name = name;return 12;
}
const person = new Person("lucas");
console.log(person);

手写new

思路:new的过程是创建一个空对象,继承构造函数的原型,在新的对象上执行构造函数,如果构造函数有返回值并且是对象类型,那么返回该对象,否则返回新对象

function myNew(constructor, ...args) {let newObj = Object.create(constructor.prototype);//执行构造函数,绑定this到newObj,接收构造函数的返回值let result = constructor.apply(newObj, args);return typeof result == "object" ? result : newObj;
}
function Person(name) {this.name = name;return { 1: 1 };
}let person = myNew(Person, "lucas");
console.log(person);

实例、构造函数和原型对象三者关系

在浏览器中可以通过__proto__访问原型对象,但是在代码里建议使用es6提供的Object.getPrototypeOf(对象)方式获取原型对象。以下为了方便,使用__proto__

__proto__和prototype关系__proto__constructor对象独有的,原型也是对象,所以原型也有constructor构造函数和__proto__指针。prototype属性是函数独有的 

实例可以通过隐式原型__proto__访问构造函数的原型对象。构造函数又可以通过prototype属性访问到构造函数的原型对象。因此中间有这样一个相等关系:实例.__proto__==构造函数.prototype

实例可以通过constructor构造器访问自己的构造函数。这个特性通常也可用于判断对象的类型,但是对象的constructor是可以更改的,因此使用constructor也会不准确。默认情况:实例.constructor=构造函数

原型对象通过constructor指向构造函数,第三个等式:构造函数.prototype.constructor===构造函数

如果构造函数继承了父类的构造函数,同理,构造函数的原型对象通过隐式原型__proto__访问原型对象。等式:构造函数的原型对象.__proto__==父类构造函数的原型

注意是原型对象有__proto__而不是构造函数,构造函数本身只有prototype属性!

当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。

测试题

测试题一:下面代码输出结果

function Foo() {getName = function () {console.log(1);};return this;
}
Foo.getName = function () {console.log(2);
};
Foo.prototype.getName = function () {console.log(3);
};
var getName = function () {console.log(4);
};
function getName() {console.log(5);
}//请写出以下输出结果:
Foo.getName(); 
getName();
Foo().getName(); 
getName(); 
new Foo.getName(); 
new Foo().getName(); 
new new Foo().getName(); 

思路:考察构造函数,普通函数、var变量提升、 函数声明和函数表达式重名时的处理逻辑。

  • 任何函数都是普通对象,都可以添加自己的属性
  • 任何函数都可以当做构造函数被new调用,且任何函数都有原型对象prototype属性,只不过大部分函数不是标准的构造函数内容而已

执行Foo.getName()时,获取的是Foo的属性getName,输出2。属性通过key冒号的形式显示定义或者使用点.操作符,因此是2。

执行getName()方法时,执行的是全局的getName方法。观察代码,同时使用var声明了一个函数表达是和函数声明getName。由于函数声明大于变量的声明,因此函数声明提升,但是代码执行到赋值语句时,getName函数的函数体被重写了。因此函数表达式和函数声明重复时,函数表达式的函数体是最终要执行的函数。这里getName输出4

Foo().getName(),这个语句的意思是先执行Foo方法,由于Foo返回的是this,作为普通方法执行,this执行全局对象,此时getName仍然是全局的getName方法,但是观察Foo函数内部,getName方法又被重写了,所以输出的是1

执行getName方法,全局的getName方法,跟上面同步,也是1

new Foo.getName()将Foo.getName()作为构造函数执行,输出2

new Foo().getName()将Foo作为构造函数执行,返回的实例调用getName,因此访问的是Foo原型对象上的getName方法,输出3

最后一个new new Foo().getName(),仍然输出3

 测试题二:下面代码的输出结果

// example1
var a = {},b='123',c=123
a[b] = 'b'
a[c] = 'c'
console.log(a[b])// example2
var a = {},b=Symbol('123'),c=Symbol('123')
a[b] = 'b'
a[c] = 'c'
console.log(a[b])// example3
var a = {},b={key:'123'},c={key:'456'}
a[b] = 'b'
a[c] = 'c'
console.log(a[b])

思路:JS对象key的数据类型

  • 只能是字符串和symbol类型
  • 其他类型会被转化为字符串
  • 字符串会直接调用toString方法
// example1
var a = {},b='123',c=123
a[b] = 'b'
a[c] = 'c' // c是数字会被转为字符串a['123'] = 'c' 覆盖上一个'b'
console.log(a[b]) // c// example2
var a = {},b=Symbol('123'),c=Symbol('123')
a[b] = 'b' // Symbol('123')是一个独一无二的值,每次都不一样。不会被覆盖
a[c] = 'c' 
console.log(a[b]) // b// example3
var a = {},b={key:'123'},c={key:'456'}
a[b] = 'b' // 对象作为key被被转为'[object Object]'  a['[object Object]'] = 'b'
a[c] = 'c'
console.log(a[b]) // c

 答案:c    b   c

 

这篇关于JavaScript原理篇——理解对象、构造函数、原型、继承的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/973934

相关文章

SpringBoot中四种AOP实战应用场景及代码实现

《SpringBoot中四种AOP实战应用场景及代码实现》面向切面编程(AOP)是Spring框架的核心功能之一,它通过预编译和运行期动态代理实现程序功能的统一维护,在SpringBoot应用中,AO... 目录引言场景一:日志记录与性能监控业务需求实现方案使用示例扩展:MDC实现请求跟踪场景二:权限控制与

Java NoClassDefFoundError运行时错误分析解决

《JavaNoClassDefFoundError运行时错误分析解决》在Java开发中,NoClassDefFoundError是一种常见的运行时错误,它通常表明Java虚拟机在尝试加载一个类时未能... 目录前言一、问题分析二、报错原因三、解决思路检查类路径配置检查依赖库检查类文件调试类加载器问题四、常见

Java注解之超越Javadoc的元数据利器详解

《Java注解之超越Javadoc的元数据利器详解》本文将深入探讨Java注解的定义、类型、内置注解、自定义注解、保留策略、实际应用场景及最佳实践,无论是初学者还是资深开发者,都能通过本文了解如何利用... 目录什么是注解?注解的类型内置注编程解自定义注解注解的保留策略实际用例最佳实践总结在 Java 编程

Java 实用工具类Spring 的 AnnotationUtils详解

《Java实用工具类Spring的AnnotationUtils详解》Spring框架提供了一个强大的注解工具类org.springframework.core.annotation.Annot... 目录前言一、AnnotationUtils 的常用方法二、常见应用场景三、与 JDK 原生注解 API 的

Java controller接口出入参时间序列化转换操作方法(两种)

《Javacontroller接口出入参时间序列化转换操作方法(两种)》:本文主要介绍Javacontroller接口出入参时间序列化转换操作方法,本文给大家列举两种简单方法,感兴趣的朋友一起看... 目录方式一、使用注解方式二、统一配置场景:在controller编写的接口,在前后端交互过程中一般都会涉及

Java中的StringBuilder之如何高效构建字符串

《Java中的StringBuilder之如何高效构建字符串》本文将深入浅出地介绍StringBuilder的使用方法、性能优势以及相关字符串处理技术,结合代码示例帮助读者更好地理解和应用,希望对大家... 目录关键点什么是 StringBuilder?为什么需要 StringBuilder?如何使用 St

使用Java将各种数据写入Excel表格的操作示例

《使用Java将各种数据写入Excel表格的操作示例》在数据处理与管理领域,Excel凭借其强大的功能和广泛的应用,成为了数据存储与展示的重要工具,在Java开发过程中,常常需要将不同类型的数据,本文... 目录前言安装免费Java库1. 写入文本、或数值到 Excel单元格2. 写入数组到 Excel表格

Java并发编程之如何优雅关闭钩子Shutdown Hook

《Java并发编程之如何优雅关闭钩子ShutdownHook》这篇文章主要为大家详细介绍了Java如何实现优雅关闭钩子ShutdownHook,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起... 目录关闭钩子简介关闭钩子应用场景数据库连接实战演示使用关闭钩子的注意事项开源框架中的关闭钩子机制1.

Maven中引入 springboot 相关依赖的方式(最新推荐)

《Maven中引入springboot相关依赖的方式(最新推荐)》:本文主要介绍Maven中引入springboot相关依赖的方式(最新推荐),本文给大家介绍的非常详细,对大家的学习或工作具有... 目录Maven中引入 springboot 相关依赖的方式1. 不使用版本管理(不推荐)2、使用版本管理(推

Java 中的 @SneakyThrows 注解使用方法(简化异常处理的利与弊)

《Java中的@SneakyThrows注解使用方法(简化异常处理的利与弊)》为了简化异常处理,Lombok提供了一个强大的注解@SneakyThrows,本文将详细介绍@SneakyThro... 目录1. @SneakyThrows 简介 1.1 什么是 Lombok?2. @SneakyThrows