一道阿里面试题带你深入认识js包装类

请问下面这道题的输出是什么,并解释其原因

ini 复制代码
var str = 'abc'
str += 1
var test = typeof str
if(test.length = 6){
    test.sign = 'typeof的返回结果可能为String'
}
console.log(test.sign);

初看这个题目,不就是直接打印出"typeof的返回结果可能为String"嘛?真有这么简单吗,我们现在仔细分析下,str = 'abc' + 1它的typeof是String类型(这里应该都懂吧,字符串与数值合并后类型转换还是字符串)于是返回了一个"string"因此test就是"String"字符串,它的长度为6,于是进入if中,这里给了个key属性给到String,这种操作不是只有对象才可以有吗?,怎么会正常打印,于是你给出的答案是报错。

但其实这道题目答案是

javascript 复制代码
undefined

纳尼?情报是假的!

还是老样子,我们带着这些疑问进入今天的主题,包装类,但是在讲包装类之前我们得先复习下js数据类型这些内容

js数据类型

js数据类型分为两大类:原始数据类型和引用数据类型

原始数据类型:

  1. number
  2. String
  3. Boolean
  4. undefined
  5. null

[^]: es6以及之后还出了两种,分别是Symbol 和Bigint ,这里我们不讨论也不做介绍

引用数据类型

  1. 对象
  2. 数组
  3. 函数
  4. 日期
  5. 正则表达式

我们今天不对这些数据类型做介绍,不过我们重点会讲到基本数据类型和对象,这里我需要先说下,基本数据类型相比较对象是不能挂属性与方法的

对象

我们先来聊聊对象,在程序员这个圈子中广为流传一个说法:万物皆对象。就是因为对象的范围实在是太广了,基本上js数据类型除了基本的其余都可以看为对象,对象里面可以存放各种数据类型,甚至数组,函数等等。

对象的创建有以下四种方法(这里只介绍四种,还有其他冷门方法我这里就不介绍了)

  1. 对象字面量

    css 复制代码
    var person = {
    	name: '小黑子',
    	age: 18
    }
  2. 构造函数

    ini 复制代码
    function Person(name, age){
    	this.name = name
    	this.age = age
    }
  3. object构造函数

    ini 复制代码
    var person = new Object()
    person.name = "小黑子"
    person.age = 18
  4. object.create()

    javascript 复制代码
    var person = Object.create()
    // 这个用的很少,但是有一个考点我们以后讲原型的时候再讲

这里的对象创建我们也就只有前两个用的多。3,4方法我这里也不讲。前面刚刚说了,对象可以存放各种数据,因此这个东西如果还是存放在调用栈中会很费空间。那么对象是如何存放的呢?

为了帮助理解,我们这里看两个例子,看完之后再结合我的解释你就会清楚对象的存放逻辑

ini 复制代码
let a = 1
let b = a
a = 2
console.log(b)

这段代码毫无疑问肯定是输出1,因为执行a赋值给b后,b的值就不会改变

如果现在换成对象会是怎样呢?

ini 复制代码
let obj = {
    name: '老八'
}
let a = obj
obj.name = '老七'
console.log(a.name);

输出结果

老七

啊?为什么啊?怎么跟上面不一样了,不应该是输出老八吗?你改obj管我a什么事情。实则不然。

之前我们讲过数据都是存放在各自的执行上下文中,而执行上下文又是存放在堆当中,当时并没有谈到对象这个特殊玩意儿,上面谈到对象这个东西可以存放各种数据类型,非常之特殊,如果他也存在调用栈中,那么将会非常不友好,容易引发内存泄漏。所以我们的js引擎针对对象这类的引用类型,专门开辟了一个空间名为堆(heap),堆里面有很多房间,每个房间都有个号码,我们执行上下文存放的对象仅仅是堆的地址。比如我们现在的obj对象在堆中1111号房间。

我们现在来进行分析一波。

全局执行上下文:obj: undefined a: undefined 准备好后执行上下文入调用栈并且开始执行自己的执行上下文。obj赋值成了一个来自堆中的地址1111,a又被赋值成了obj的地址1111,于是执行obj.name = '老七',就去堆中找到name这个key,修改它的值为老七,此时的obj里面就是name: '老七'了,最终打印a.name,a是obj的地址,于是也去堆中找到1111,发现里面是老七在,最终输出老七!

当我们想要打印对象的某个值的时候,我们只需要obj.key即可,其实还可以用中括号来取值,不过key要用引号,obj['key']一定要用引号,不然这个key会被当成变量,最终报错。

javascript 复制代码
let obj = {
    name: '丁真',
    age: 18
}
// console.log(obj.age)
console.log(obj['name'])

那我们如果点一个没有的key会如何呢?

ini 复制代码
let obj = {
    name: '丁真',
    age: 18
}
obj.girlFriend = '翠花'
console.log(obj)

输出结果

css 复制代码
{ name: '丁真', age: 18, girlFriend: '翠花' }

所以说,这样是可以添加属性的。

那我再考察一下大家,下面这样会是如何输出呢

ini 复制代码
let obj = {
    name: '丁真',
    age: 18
}
obj.girl = '小红' 
let girl = 'girlFriend'
console.log(obj);

输出结果

css 复制代码
{ name: '丁真', age: 18, girl: '小红' }

解释下,obj.girl是一个key,而let girl是一个变量,我们最终打印的是obj,与外面的变量无关,这样说可能不够形象。那我把这个例子再改一下

ini 复制代码
let obj = {
    name: '丁真',
    age: 18
}
let girl = 'girlFriend'
obj[girl] = '小红'
console.log(obj);

输出结果

css 复制代码
{ name: '丁真', age: 18, girlFriend: '小红' }

聪明的你肯定发现了,这里的obj[girl]里面并没有用引号,所以这个key是一个变量,他由他的值决定,所以最后的key为girlfriend。

如果我们要删除对象的某个属性,我们直接delete掉就可

arduino 复制代码
delete obj.girlFriend

上面详细介绍了对象的存储,以及基本的增删改查

下面我们来看一下构造函数

构造函数

构造函数是用来创建对象的函数,一般都是大写字母开头,这只是一种命名规范,其实跟普通函数没有区别。并且构造函数一般都是和new一起使用来实例化对象用的。

结合下面的范例

ini 复制代码
function Person(name, age, sex){
    this.name = name
    this.age = age
    this.sex = sex
}
let p = new Person('小黑子', 19, 'male')
console.log(p);

这里的Person就是一个构造函数,下面的p就是一个实例化对象,必须是用了new才能实例化对象。如果我们这里没有用new得话相当于p是调用了这个函数,再打印p因为没有值,最终打印undefined。

这里我需要介绍下new这个东西的原理,new这个东西其实就是给实例化对象返回了this所有的key.value,既然是key.value那么这个this其实就是对象 ,也就是说new相当于在构造函数中创建了一个this对象,构造函数里面的代码就相当于往this里面挂属性于是最终返回this对象

javascript 复制代码
function Person(name, age, sex){
    this.name = name
    this.age = age
    this.sex = sex
// 所以上面的代码就相当于往this里面放属性
//     let this = {
//         name: name,
//         age: age,
//         sex: sex
//     }
//     return this
}
 let p = Person('小黑子', 19, 'male')

既然这样的话,那肯定有小伙伴要问了,我们直接在构造函数中模拟一个this出来不就可以了,我们就弄个冒牌货that

ini 复制代码
function Person(name, age, sex){
    var that = {}
    that.name = name
    that.age = age
    that.sex = sex
    return that
}
let p1 = Person('小黑子',89,'female')
let p2 = Person('大黑子',88,'female')
console.log(p1);
console.log(p2)

输出结果

css 复制代码
{ name: '小黑子', age: 89, sex: 'female' }
{ name: '大黑子', age: 88, sex: 'female' }

这样效果是一样的啊!不过我们不推荐这样写,new里面有更深层的内容。我们后面讲原型(prototype)的时候再提。

包装类

js包装类指的是js基本数据类型中(或者原始数据类型)数字,字符串,布尔值可以通过包装对象来扩展功能。他们三个的包装对象分别为Number、String 和 Boolean。

为了理解包装类我们可以先看下对象这个东西,大家看下下面这个东西会输出什么

ini 复制代码
var obj = {}
console.log(obj.a);

输出结果

javascript 复制代码
undefined

啊?为什么是undefined?obj是个空对象,a这个属性都没有声明过,怎么会存在,undefined应该是定义了但是没有赋值。是不是很奇怪!对象很特殊,即使是查找没有存在的属性也是undefined,而不是报错!这个查找过程我们后面讲原型的时候还会详细讲一下,里面有个比较复杂的过程。

我们再来看下下面这个例子

ini 复制代码
var num = 123
num.abc = 'abc'
console.log(num.abc);

输出结果

javascript 复制代码
undefined

啊?这又是什么个情况,num不是原始数据类型吗,原始数据类型不是不能挂属性和方法吗,既然不能那这里应该是报错啊。这里又是一个学问!其实这个代码在js执行引擎看来是下面这样的

ini 复制代码
var num = 123
num.abc = 'abc'
// 上面这一行代码相当于var num = new Number(4) 然后 num.abc = 'abc'
// 最后delete num.abc

num是个原始数据类型既然你要添加属性,我就用包装类进行实例化,将他变成一个对象,然后就有正当理由去拥有属性了,但是最后我们又要删掉这一属性,因为他是原始数据类型不能拥有属性。

可以可以,既然如此,删掉了这个属性后也应该是报错啊,为什么又是undefined

我们来看最后这个打印代码在js执行引擎看来是什么样子

arduino 复制代码
console.log(num.abc)
// new Number().abc

什么?这个原始数据类型又被包装类进行声明了?然后加上了这个属性!

没错这个过程很奇怪,原始数据类型添加属性,在js执行引擎看来会先用包装类进行实例化然后有了这个属性之后又被删除,后面想要打印这个属性时又会被再次实例化,并且带上了这个属性,因此是undefined。这一过程我们称之为隐式包装类。像是这个东西,就是面试官问你原始数据类型.属性,为何不报错的一个标准答案。

噢对了,好像还没给出过包装对象怎么用的,且看下面

dart 复制代码
var num = 4
// 等同于下面
var num = new Number(4)

String 和Boolean同理

我们下面再来看一个考点

ini 复制代码
var arr = [1, 2, 3, 4, 5]
arr.length = 2
console.log(arr)
var str = 'abcde'
str.length = 2
console.log(str)

输出结果

csharp 复制代码
[1, 2]
abcde

这里面试官就会问你为什么会这样输出

之前我们说过原始数据类型是没有属性的,但是刷过算法题的老兄肯定知道字符串是有.length这个属性的。很特殊对吧!其实对于字符串本身确实是没有任何属性的,但是我们的js执行引擎执行代码的时候会将字符串包装成字符串对象,也就是我们说的包装类,而这个包装类就是有length属性的,这里你别问我为什么,人家就是这样设计的,js引擎就是为字符串对象(包装之后的,或者说new String 之后)提供了这样一个内置方法和属性。但是为什么length属性改不动呢,js引擎执行str.length = 2的时候相当于new String('abcde').length = 2之后又删掉。其实聪明的你这里就可以总结到,字符串对象的length属性是只读属性,你是无法对其进行操作的!

arr的length可以更改是因为数组是一种特殊的对象,他允许你灵活地对他的属性和方法进行操作。

收回开头的阿里面试题

ini 复制代码
var str = 'abc'
str += 1
var test = typeof str
if(test.length = 6){
    test.sign = 'typeof的返回结果可能为String'
}
console.log(test.sign);

同样,读到这里你可以先自己思考下这个结果为undefined的原理再来看我下面的分析是否与你一致

这里想要顺便说下typeof这个api,他可以空格检测对象,也可以用把检测对象放入括号中

rust 复制代码
typeof str || typeof(str)

并且他最后给的返回值是一个字符串,如果这里的str是对象,就会返回'object',如果是数字,就会返回'number',其余类型同理。

这里也解释下为什么字符串和数字相加后不是数值相加,懂的话你就跳过这一段。我的处女作文章就有说过js是一门弱类型语言,这里的体现就是它能够自动进行类型转换,字符串和数字相加最后被转换为字符串就是其中规则之一,如果你想要转换成字符串你可以parseInt(str) + 1。

所以typeof str返回一个'string'并且赋值给了test,所以test = 'string'它的长度为6,没有任何问题。进入if中,test.sign = 'typeof的返回结果可能为String'对于js引擎看来就是

javascript 复制代码
new String('string').sign = 'typeof的返回结果可能为String'
delete String('string').sign

执行console.log(test.sign)对于js引擎看来就是

javascript 复制代码
new String('string').sign

这个属性没有任何值,因此打印出undefined。

这篇文章元素有点多,主要讲了包装类,对象,构造函数,数据类型,堆。希望看完对你有所帮助!知识点很难,但是博主我尽量站在小白的角度用自己语言带你理解透彻。


如果觉得本文对你有帮助的话,可以给个免费的赞吗[doge] 还有可以给我的gitee链接codeSpace: 记录coding中的点点滴滴 (gitee.com)点一个免费的star吗[星星眼]

相关推荐
Pandaconda6 分钟前
【Golang 面试题】每日 3 题(六十五)
开发语言·经验分享·笔记·后端·面试·golang·go
Violet51524 分钟前
ECMAScript规范解读——this的判定
javascript
知识分享小能手1 小时前
Html5学习教程,从入门到精通,HTML5 简介语法知识点及案例代码(1)
开发语言·前端·javascript·学习·前端框架·html·html5
代码猪猪傻瓜coding1 小时前
【模块】 ASFF 模块
人工智能·深度学习
IT、木易1 小时前
大白话React第二章深入理解阶段
前端·javascript·react.js
Good Note1 小时前
Golang的静态强类型、编译型、并发型
java·数据库·redis·后端·mysql·面试·golang
晚安7201 小时前
Ajax相关
前端·javascript·ajax
bin91531 小时前
DeepSeek 助力 Vue 开发:打造丝滑的单选按钮(Radio Button)
前端·javascript·vue.js·ecmascript·deepseek
那就可爱多一点点2 小时前
超高清大图渲染性能优化实战:从页面卡死到流畅加载
前端·javascript·性能优化
老A的AI实验室3 小时前
通俗理解Test time Scaling Law、RL Scaling Law和预训练Scaling Law
人工智能·深度学习·算法·chatgpt·llm·agi·rl