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

相关文章

Java中对象的创建和销毁过程详析

《Java中对象的创建和销毁过程详析》:本文主要介绍Java中对象的创建和销毁过程,对象的创建过程包括类加载检查、内存分配、初始化零值内存、设置对象头和执行init方法,对象的销毁过程由垃圾回收机... 目录前言对象的创建过程1. 类加载检查2China编程. 分配内存3. 初始化零值4. 设置对象头5. 执行

SpringBoot整合easy-es的详细过程

《SpringBoot整合easy-es的详细过程》本文介绍了EasyES,一个基于Elasticsearch的ORM框架,旨在简化开发流程并提高效率,EasyES支持SpringBoot框架,并提供... 目录一、easy-es简介二、实现基于Spring Boot框架的应用程序代码1.添加相关依赖2.添

通俗易懂的Java常见限流算法具体实现

《通俗易懂的Java常见限流算法具体实现》:本文主要介绍Java常见限流算法具体实现的相关资料,包括漏桶算法、令牌桶算法、Nginx限流和Redis+Lua限流的实现原理和具体步骤,并比较了它们的... 目录一、漏桶算法1.漏桶算法的思想和原理2.具体实现二、令牌桶算法1.令牌桶算法流程:2.具体实现2.1

SpringBoot中整合RabbitMQ(测试+部署上线最新完整)的过程

《SpringBoot中整合RabbitMQ(测试+部署上线最新完整)的过程》本文详细介绍了如何在虚拟机和宝塔面板中安装RabbitMQ,并使用Java代码实现消息的发送和接收,通过异步通讯,可以优化... 目录一、RabbitMQ安装二、启动RabbitMQ三、javascript编写Java代码1、引入

spring-boot-starter-thymeleaf加载外部html文件方式

《spring-boot-starter-thymeleaf加载外部html文件方式》本文介绍了在SpringMVC中使用Thymeleaf模板引擎加载外部HTML文件的方法,以及在SpringBoo... 目录1.Thymeleaf介绍2.springboot使用thymeleaf2.1.引入spring

Java实现检查多个时间段是否有重合

《Java实现检查多个时间段是否有重合》这篇文章主要为大家详细介绍了如何使用Java实现检查多个时间段是否有重合,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录流程概述步骤详解China编程步骤1:定义时间段类步骤2:添加时间段步骤3:检查时间段是否有重合步骤4:输出结果示例代码结语作

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Java判断多个时间段是否重合的方法小结

《Java判断多个时间段是否重合的方法小结》这篇文章主要为大家详细介绍了Java中判断多个时间段是否重合的方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录判断多个时间段是否有间隔判断时间段集合是否与某时间段重合判断多个时间段是否有间隔实体类内容public class D

IDEA编译报错“java: 常量字符串过长”的原因及解决方法

《IDEA编译报错“java:常量字符串过长”的原因及解决方法》今天在开发过程中,由于尝试将一个文件的Base64字符串设置为常量,结果导致IDEA编译的时候出现了如下报错java:常量字符串过长,... 目录一、问题描述二、问题原因2.1 理论角度2.2 源码角度三、解决方案解决方案①:StringBui

Java覆盖第三方jar包中的某一个类的实现方法

《Java覆盖第三方jar包中的某一个类的实现方法》在我们日常的开发中,经常需要使用第三方的jar包,有时候我们会发现第三方的jar包中的某一个类有问题,或者我们需要定制化修改其中的逻辑,那么应该如何... 目录一、需求描述二、示例描述三、操作步骤四、验证结果五、实现原理一、需求描述需求描述如下:需要在