本文主要是介绍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)有一些基本的要求:
-
唯一性:每个键必须是唯一的,不能有两个相同的键存在于同一个对象中。如果有重复的键,后面的值会覆盖前面的值。
-
数据类型:键可以是以下几种数据类型:
- 字符串:字符串是JavaScript中最常见的键类型,例如
{"name": "John"}
。 - 符号(Symbol):从ES6开始,可以使用
Symbol
创建独一无二的键,例如let obj = { [Symbol('key')]: 'value' }
。 - 基本数据类型:其他基本数据类型(如数字、布尔值)会自动转换为字符串,例如
let obj = { 1: 'one', true: 'yes' }
,但不推荐,因为可能会与预期不符。
- 字符串:字符串是JavaScript中最常见的键类型,例如
-
可枚举性:默认情况下,对象的键是可枚举的,这意味着它们在
for...in
循环中会被遍历到。可以通过Object.defineProperty
来改变键的可枚举性。 -
保留字:JavaScript有一些保留字不能用作对象的键,如
class
,for
,function
等,如果尝试用这些词作为键,会引发错误。 -
空值:
null
和undefined
也不能作为键,因为它们在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原理篇——理解对象、构造函数、原型、继承的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!