对象与包装类:揭开JavaScript背后的神秘面纱
前言
在JavaScript这门语言中有这么一条内定的规则:属性和方法是对象独有的,那么按照正常的思维方式,如果我们往一个原始类型上添加属性或者尝试访问一个对象上不存在的属性时,打印它得到的结果应该是报错信息,但是运行以下代码你会惊奇地发现最后打印出的竟然是undefined
。这不是自相矛盾吗?!
想要明白其中的原理我们就要深入了解包装类的过程,而在学习包装类之前我们先一起来学习一些有关对象的前置知识吧,只有把所有准备工作完成了,我们才能在最后成功升级打怪,通关阿里的一道面试题ヾ(◍°∇°◍)ノ゙
var num = 123
num.abc = 'abc'
console.log(num.abc); // undefined
var obj = {}
console.log(obj.color); // undefined
对象
在JavaScript中,数据类型被分为两大类:基本/原始数据类型和引用/复杂数据类型
- 基本数据类型:存储在栈中的简单数据段,包含7种类型:number, string, boolean, undefined, null, symbol, bigint。
- 引用数据类型:存储在堆中的对象,变量中保存的只是对象的引用(即内存地址),包含两种:function,object。
Object也就是对象,是通过键值对也称为属性来存储数据的,每个属性都有一个名称(键)和对应的值。对象的值可以是任何数据类型,包括原始类型和其他的对象,比如说数组就是一种特殊的对象。
创建对象的多种方式
JavaScript提供了多种方式来创建对象,以满足不同的编程场景和需求。下面将详细讲解这些方式,并结合代码示例进行说明。
创建对象字面量
创建对象字面量是最直接且常用的创建对象的方式,通过在大括号{}
中直接定义属性和方法,就可以快速创建一个对象了。
var person = {
name: 'hhh',
age: 18,
eat: function () {
console.log('I am eating')
this.health --
}
}
person.eat();
调用系统自带的构造函数 new Object()
使用new Object()
并传入一个对象字面量作为参数就可以创建一个对象了,使用这种方式创建的对象与第一种方式创建的对象没有任何的区别,至于原因你会在后面的包装类知识中找到的。
var obj = new Object() // 创建对象实例
调用自定义构造函数
自定义构造函数一般在批量化创建对象时才使用,允许你定义创建对象的模板。
通过new
关键字调用构造函数时,JavaScript会创建一个新的空对象,并将this
指向这个新对象,然后执行构造函数中的代码,最后返回这个新对象(如果构造函数没有显式返回其他值)。
function Car (color) {
this.name = 'su7'
this.price = 100000
this.color = color
}
let car = new Car('red')
Object.create()方法
Object.create()
方法允许你使用现有的对象来提供新创建的对象的__proto__
,从而允许新对象继承原型链上的属性和方法。
ES6 类(Class)
ES6引入了类的概念,使得对象的创建更加面向对象和模块化。类提供了一种更清晰、更易于理解的方式来创建具有相同属性和方法的对象。
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
eat() {
console.log('I am eating')
}
}
包装类
了解完前面的知识后,我们再结合这段话原始值是不能拥有属性和方法的,属性和方法是对象独有的和以下给出的代码来思考一下这些问题:
- 是什么原因导致了这个看似荒谬的悖论?
- 为什么一个属于基本数据类型的数字能添加一个属性?
- 为什么在拥有属性后还能继续四则运算?
- 为什么这两段代码中num都添加了属性,一个可以访问,一个却不能访问到属性呢?
var num = new Number(123) // 悖论:new一个构造函数 得到的是一个对象,但是一个对象不能直接参与运算
num.abc = 'abc' // 实例对象可以添加属性
console.log(num.abc); // abc
console.log(num * 2); // 246
var num = 123
num.abc = 'abc' // 原始类型上添加属性
console.log(num.abc); // undefined
隐式转换
为什么一个属于基本数据类型的数字能添加一个属性?
这些都是因为JavaScript引擎自发进行的包装类过程,也就是隐式转换。当JavaScript引擎编译var num = 123
这段代码时,会自动进行转换为var num = new Number(123)
,这也就是在前文中我说到调用系统自带的构造函数创建对象与直接创建对象字面量的方式得到的对象是一模一样的原因,因为无论是哪种方式创建,最后JavaScript引擎都会编译为用系统自带的构造函数创建。这些基本数据类型除了null类型外都有属于自己的构造函数,而new一个构造函数得到的必然是一个对象,所以当你向一个原始类型上添加属性时不会报错。
为什么在拥有属性后还能继续四则运算?
尽管JavaScript引擎编译后得到的是对象,可是当它执行到num * 2
的四则运算代码时,它会往前去找num是什么,然后发现了num被我们定义且要求为一个数字而不是一个对象,于是num就被认定为 number 类型可以参与四则运算,而不是对象。
为什么这两段代码中num都添加了属性,一个可以访问,一个却不能访问到属性?
第一段代码中我们是通过一个构造函数来创建的num,所以添加上属性后,JavaScript引擎再次访问num,会认为我们想要的是一个对象,因此不会删除添加上去的属性abc,最后仍能访问到这个属性。
第二段代码中我们是通过创建一个字面量来得到的num,添加上属性后,JavaScript再次访问num,会认为我们想要的本来就是一个数字而非对象,所以它会删除这个属性delete new Number(123).abc
,当访问这个属性时就只会得到undefined
。
我们再来看一个例子,通过以上讲解,你知道最后的输出是什么吗?
let arr = [1, 2, 3, 4];
arr.length = 2
console.log(arr);
let str = 'abcd'
str.length = 2
console.log(str);
数组是一种特殊的对象,而字符串是原始类型,它们在JavaScript中会被编译成这样:
new Array(1, 2, 3, 4).length = 2
console.log(arr); // [1, 2]
new String('abcd').length = 2
delete new String('abcd').length // 因为str.length被移除了,所以改不动
console.log(str); // abc
阿里面试题详解
希望以上的讲解能够为你解惑,接下来是一道阿里的面试题,涉及的知识点就是包装类,如果你不了解JavaScript引擎的特点与包装类的知识,遇到这道题确实是会一头雾水,让我们一起来康康吧O(∩_∩)O
typeof
操作符用于获取一个变量或表达式的类型,并返回一个表示该类型的字符串。
// 阿里的面试题
var str = 'abc'
str += 1
console.log(str); // 输出 'abc1'
var test = typeof(str)
if(test.length === 6){
test.sign = 'typeof 返回值结果是 String'
}
console.log(test.sign); // 输出结果为 undefined
str += 1;
这行代码将数字 1 转换为字符串,然后将它与 str 的当前值拼接,最终 str
的值变为 'abc1'
。第5行代码返回一个字符串 'string'
,表示 str
的类型是字符串。因此,test
的值被赋为 'string'
,符合if语句中的判断条件,test上添加了一个属性 sign
,但是由于test是字符串类型,JavaScript引擎会执行删除操作将sign属性删除,所以在第9行代码中得到的结果是undefined
。
结语
在 JavaScript 中,理解包装类和隐式转换的机制是非常重要的。通过深入探讨这些机制,我们可以更好地理解为什么某些代码行为看似违反直觉却有其内在逻辑。当我们创建一个原始类型时,JavaScript 引擎会将原始值包装成对应的对象类型,这使得我们可以在原始值上调用属性和方法。然而,这些临时包装对象在操作完成后被销毁,因此无法持久保存我们添加的属性,所以当再次访问这个属性时只会得到undefined。
在面对复杂的面试题时,理解这些底层机制可以帮助我们做出正确的判断和解释。希望通过这篇文章,你能够更加清晰地理解 JavaScript 的工作原理,并在实际编程和面试中游刃有余。感谢观看Thanks♪(・ω・)ノ
转载自:https://juejin.cn/post/7395969637877907510