JavaScript中的基本数据类型

在JS中,有6种基本数据类型:

  1. string
  2. number
  3. boolean
  4. null
  5. undefined
  6. Symbol(ES6)

除去这六种基本数据类型以外,其他的所有变量数据类型都是Object。基本类型的操作在JS底层中是这样实现的:

// 1. 申请一块内存,存储foo变量的内容为1
let foo = 1
// 2. 定义foo为1时,foo的数据类型是number
typeof foo // "number"
// 3. 我们知道,const的意思是constant(常量,无法改变的)
const bar = foo
// 4. 修改值时,新申请了一块内存存储foo的内容为2
foo = 2
// 4. 则会发现,foo已经是2了,bar仍然是1
console.log(foo) // 2
console.log(bar) // 1
由此可见,我们定义的变量实际上都是指针。基本数据类型的修改实际上是新申请一块内存地址,将这个指针指向新的内存地址。使用const定义变量,实际上相当于定义了一个指针常量,指向固定的地址不能被修改。

JavaScript中的对象

定义和修改对象

我们来试着从变量定义的执行结果看出它在底层的执行方式:

// 1. 定义一个对象
const obj = {
foo: 1
}
// 2. 定义一个新变量与其相等
const anotherObj = obj
// 3. 修改这个对象
obj.foo = 2
// 4. 发现两个对象都修改了
console.log(obj)
console.log(anotherObj)
由此可见,JS中对象的赋值是一种浅拷贝。

熟悉了对象的本质以后,我们要逐步了解对象有哪些特性。

对象的属性与方法

实际上,在学习一般高级语言的时候,应该先介绍类的属性与方法(共性),才介绍实例化类产生的对象如何使用(特性)。但由于JS是以原型、对象为主的语言,类只能在ES6中以语法糖的形式存活,我们只能先从对象入手,反推类的性质。

对象其实就是一些属性和一些方法的集合。而对象的属性和方法要深究,其实也是非常复杂的问题(光看内置对象Object以及Object.prototype上有多少方法处理对象的属性就知道不简单):

属性

描述符

每个属性上有描述符号。所谓的描述符号,是一些键值对,它们描述了对于这个属性是否能操作、是否能枚举等等的所有特性。

描述符号只能是数据描述符和存取描述符两个里面的一个(在一般的声明中,属性默认含有的是数据描述符)。

首先,这两种描述符公有的两个属性是:configurable(这个属性的描述符是否能被修改、以及这个属性是否能被delete运算符删除)和enumerable(是否能被枚举)。

然后是数据描述符,顾名思义,它定义了value(值)和writable(值是否能被赋值语句修改)。

最后是存取描述符,同样的顾名思义,它定义了这个属性的get(读取时执行的函数)和set(修改时执行的函数)。

定义和修改属性

我们可以通过Object.defineProperty来具体地配置一个属性的描述符:

const foo = {}
// 1. 数据描述符
Object.defineProperty(foo, 'bar', {
// 1.1. 固定了是数据描述符不能被修改
configurable: false,
// 1.2. 设置该属性可以枚举
enumerable: true,
// 1.3. 值为3
value: 3,
// 1.4. 无论怎么赋值更新foo.bar,它的值仍然是3
writable: false
})
// 2. 存取描述符
let baz = 3
Object.defineProperty(foo, 'baz', {
// 2.1. 固定了是存取描述符不能被修改
configurable: false,
// 2.2. 设置该属性可以枚举
enumerable: true,
// 2.3. 使用foo.baz读取时,会顺带输出这句话
get: function () {
console.log('The getter is called.')
return baz
},
// 2.4. 使用赋值语句为foo.baz赋值时,会顺带输出这句话
set: function (value) {
console.log('The setter is called.')
baz = value
}
})
由此可见,定义属性时可以根据自己的需求修改默认的描述符。

了解到这里,我们不难联想到,著名的前端框架Vue实现数据的双向绑定,实际上就是利用了这个存取描述符。我们在编写Vue代码时,定义Vue对象中data属性的值。Vue在编译过程中,首先收集了这个值的所有依赖(也就是它在我们代码中出现的各种地方),然后利用Object.defineProperty,把属性的描述符改成存取描述,并在setter中修改所有的依赖,通知视图更新。这样就有了我们觉得非常神奇的数据双向绑定。

遍历属性

对属性的常用操作除了定义与修改,还有遍历。最常用的遍历方法是:

// 两种方法,都只能遍历enumerable的属性
const obj = {
foo: 1,
bar: 2,
baz: 3
}
// 1. Object.keys
let objAttrs = Object.keys(obj)
// 2. for...in...
let objAttrs = []
for (let key in obj) {
objAttrs.push(key)
}
// 以上两种遍历的方法数量和顺序均一致
// 如果不希望遍历原型上的属性,还可以使用Object.hasOwnProperty进行过滤
遍历时需要考虑到属性是否能被枚举以及原型上的属性是否需要被遍历到。

方法

函数调用

函数有总共四种调用模式,这四种调用模式其实都是围绕着this指向的不同而定的(下面的全局在浏览器环境中表示window,在node环境中表示global):

  1. 普通函数调用 —— this指向全局
  2. 方法调用 —— this指向方法所定义的对象
  3. 构造器调用 ——(使用new关键字时)this指向当前函数对象(函数本身就是对象)
  4. (call、apply和bind)调用 —— this指向(call、apply和bind)函数的第一个参数
方法是什么

方法就是定义在类或者对象上,用来处理对象有关数据的函数。简而言之,方法就是函数的子集。方法特别于其他函数的点在于,它的this是指向当前对象的。

从方法到this

由此可见,我们通过不同的方式调用函数,最终为的还是根据自己的需求定义this的指向。我们试着来区分几个例子,从而最终总结出JS中this的指向情况:

一般情况
// 定义一个对象,里面有一个输出对象自身的方法
const obj = {
foo: function () {
return this
}
}
// 直接执行obj.foo方法,正常得到obj对象
console.log(obj.foo())
// 用一个外部变量接收obj.foo方法
const fakeFoo = obj.foo
// 执行这个接收回来的方法,获得this为全局对象
console.log(fakeFoo())
上述例子说明,一般情况下,this指向的是函数被调用时所在的上下文环境。
(所谓函数调用时的上下文环境,实际上也等同于JS中的词法作用域(lexical scope),即函数作用域)
内部函数

下面再来看看内部函数的this指向:

// 定义一个对象,里面有一个方法,方法里面有一个返回this的内部函数
// 以此测试内部函数中this指向
const obj = {
foo: function () {
return function () {
return this
}
}
}
// 执行这个内部的函数,发现this指向的是全局对象
console.log(obj.foo()())
上述例子说明,内部函数中,this没有指向当前对象,而是指向的是全局。
箭头函数

当然,ES6中箭头函数的出现修复了这些问题,内部函数的this也能正确指向当前对象了:

// 仅仅把上述对象的内部函数换为箭头函数
const obj = {
foo: function () {
return () => {
return this
}
}
}
// 正确得到this为当前对象
console.log(obj.foo()())
上述例子说明,箭头函数把this绑定回了词法作用域。

但是,由于JS的词法作用域为函数作用域,以下的写法又会发生错误:

const obj = {
foo: () => {
return this
}
}
// 得到的this为全局对象
console.log(obj.foo())
上述例子说明了,由于JS词法作用域为函数作用域,箭头函数没有外部函数包着,因此是全局作用域。

但是,箭头函数强制将this绑定到函数执行的上下文环境。这导致了bind、call与apply的失效。

// 定义一个对象,里面有一个方法返回当前对象的foo属性
// 并将这个方法应用到foo为2的新对象上
const obj = {
foo: 1,
bar: function () {
const foo = this.foo
const baz = function () {
return foo
}
return baz.call({ foo: 2 })
}
}
// 得到新对象的值为2
console.log(obj.bar())
正常情况下,call方法正常地将这个方法应用到另一个对象上。
// 仅将内部返回foo的函数改为箭头函数
const obj = {
foo: 1,
bar: function () {
const foo = this.foo
const baz = () => {
return foo
}
return baz.call({ foo: 2 })
}
}
// 得到的还是旧的1,说明call方法并没有成功将this绑定到新对象上
console.log(obj.bar())
而箭头函数的this则被紧锁在了旧对象上。

总结:

  • JS的6种基本数据类型:string、number、boolean、null、undefined、Symbol
  • JS中,除了6种基本数据类型以外,其他变量都是对象,我们通过操作指针对这些对象进行处理
  • 对象的属性有两种描述符的其中一种:数据描述符(默认)和存取描述符
  • 对象的方法中,this默认指向这个对象,而方法的内部函数this默认指向全局

拓展:

  • 通过Object.defineProperty可以定义和修改某个属性的描述符
  • 普通函数中的this默认指向词法作用域,使用new定义对象、call、apply、bind等内建方法,可以修改this的指向
  • 箭头函数将this锁在了词法作用域,没办法使用call、apply、bind进行修改

林大妈的JavaScript进阶知识(一):对象与内存的更多相关文章

  1. 林大妈的JavaScript进阶知识(二):JS异步行为

    JavaScript 是单线程执行的 JavaScript运行在浏览器中.浏览器是多线程的,但只分配了其中一条给JavaScript,作为它的主线程.对于编码者来说,JavaScript是单线程的.因 ...

  2. 林大妈的JavaScript进阶知识(三):HTML5 History API

    HTML5中新增了History API,它用于管理浏览器路由跳转的一个url栈.History是window对象的一部分,它也是一个对象,因此称它是BOM(类似DOM,Browser Object ...

  3. 林大妈的JavaScript基础知识(三):JavaScript编程(2)函数

    JavaScript是一门函数式的面向对象编程语言.了解函数将会是了解对象创建和操作.原型及原型方法.模块化编程等的重要基础.函数包含一组语句,它的主要功能是代码复用.隐藏信息和组合调用.我们编程就是 ...

  4. 林大妈的JavaScript基础知识(三):JavaScript编程(3)原型

    在一般的编程语言中,我们使用继承来复用代码,做成良好的数据结构.而在JavaScript中,我们使用原型来实现以上的需求.由于JavaScript专注于对象而摒弃了类,我们要明白原型和继承的确是有差异 ...

  5. 林大妈的JavaScript基础知识(三):JavaScript编程(4)数组

    数组,是一段线性分配的,具有非常高性能的数据结构.简单地说,数组以连续的空间存储,通过整数地计算偏移量访问其中的元素,将读取修改的时间复杂度降低至O(1),我们称之为猝发式存取.是不是非常期待?没错, ...

  6. 林大妈的JavaScript基础知识(三):JavaScript编程(1)对象

    1. 对象的简单介绍与一些注意事项 JavaScript中具有几个简单数据类型:数字.字符串.布尔值.null值以及undefined值.除此之外其余所有值(包括数组.函数,甚至正则表达式)都是对象. ...

  7. 林大妈的JavaScript基础知识(一):JavaScript简史

    前言:做一名Web设计师是一件令人兴奋的事.在Web技术中,JavaScript是一个经历从被人误解到万众瞩目的巨大转变,在历史的冲击中被留存下来的个体.因为JavaScript的引导,Web开发也从 ...

  8. 林大妈的JavaScript基础知识(二):编写JavaScript代码前的一些简单工作

    在介绍JavaScript语法前,我们需要知道,学习语法必须要多利用手敲代码来巩固记忆.因此,由于JavaScript的特性,它不能像C++和Java一样独立地编译及运行,我们需要在调试运行JavaS ...

  9. 二、JavaScript语言--JS基础--JavaScript进阶篇--DOM对象 控制HTML元素

    1.认识DOM 文档对象模型DOM(Document Object Model)定义访问和处理HTML文档的标准方法.DOM 将HTML文档呈现为带有元素.属性和文本的树结构(节点树). 先来看看下面 ...

随机推荐

  1. Java容器知识总结

    剖析面试最常见问题之Java集合框架 说说List,Set,Map三者的区别? List(对付顺序的好帮手): List接口存储一组不唯一(可以有多个元素引用相同的对象),有序的对象 Set(注重独一 ...

  2. 「Main」

    这里就是我的小主页辣. My Introduction I am Louch. 姓名:楼翰诚 性别:汉纸 生日:2004/03/09(和加加林同一天呢QAQ) 星座:双鱼座 学校:义乌中学 QQ:10 ...

  3. MyBatis项目实战 快速将MySQL转换成Oracle语句

    一.前言 因项目需求,小编要将项目从mysql迁移到oracle中 ~ 之前已经完成 数据迁移 (https://zhengqing.blog.csdn.net/article/details/103 ...

  4. 字符串String类常见算法题

    1.将一个字符串进行反转.将字符串中指定部分进行反转. public class StringDemo { //方式一:转换为char[] public String reverse(String s ...

  5. Spring Cloud(二):Web服务客户端之Ribbon

    上文介绍了服务如何通过Eureka实现注册,以及如何从Eureka获取已经注册的服务列表.那么拿到注册服务列表后, 如何进行服务调用?一个简单的实现是可以从被调用服务的实例列表中选择一个服务实例,通过 ...

  6. 哪些工具可以提升PHP开发效率

    本文就我自己在开发过程中的一点经验,谈谈如何利用工具来提升开发工作的编码效率, IDE(phpstorm 收费) 一个好的IDE真的可以给开发人员节省大量的时间,我从最开始使用editplus 到su ...

  7. cogs 186. [USACO Oct08] 牧场旅行 树链剖分 LCA

    186. [USACO Oct08] 牧场旅行 ★★☆   输入文件:pwalk.in   输出文件:pwalk.out   逐字节对比时间限制:1 s   内存限制:128 MB n个被自然地编号为 ...

  8. NOIP2004普及组第3题 FBI树

    /* 1106: NOIP2004普及组第3题 FBI树 时间限制: 1 Sec 内存限制: 128 MB 提交: 10 解决: 9 [提交] [状态] [讨论版] [命题人:外部导入] 题目描述 我 ...

  9. day6 相对定位:position:relative

    相对定位:position:relative 特点:a.相对于自己原来位置的定位,以自己的左上角为基准. b.相对定位原来的位置仍然算位置,不会出现浮动现象. 以下为初始位置:(可以看出设置margi ...

  10. Java中整数值的4中表示方式u

    Java中整数值有4中表示方式:十进制.二进制.八进制和十六进制,其中二进制的整数以0b或0B开头:八进制的整数以0开头: 十六进制的整数以0x或0X开头,其中10-15分别以a-f(此处的a-f不区 ...