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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

认识、理解、分类——acm之搜索

普通搜索方法有两种:1、广度优先搜索;2、深度优先搜索; 更多搜索方法: 3、双向广度优先搜索; 4、启发式搜索(包括A*算法等); 搜索通常会用到的知识点:状态压缩(位压缩,利用hash思想压缩)。

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟 开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚 第一站:海量资源,应有尽有 走进“智听