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. teamviewer早期版本下载链接

    https://www.teamviewer.cn/cn/download/previous-versions/

  2. layui下拉选择框select不显示

    弹层layer下拉框没有样式_不可点击_没有效果_渲染失效的解决办法 一.必须给表单体系所在的父元素加上 class="layui-form" 在一个容器中设定 class=&qu ...

  3. Redis 高可用之"持久化"

    Redis高可用概述 在Redis中,实现高可用的技术主要包括:持久化.复制(读写分离).哨兵.集群. 持久化: 持久化是最简单的高可用方法(有时甚至不被归为高可用手段),主要作用是数据备份,即将数据 ...

  4. 2019年全网最热门的123个Java并发面试题总结

    前言 并发编程几乎是所有互联网公司面试必问的问题,并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能.它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰.思维缜密, ...

  5. hadoop配置环境变量

    hadoop安装包解压 tar -xvf hadoop-2.7.7.tar.gz 解压成功ll查看文件 配置环境变量 1. vi  /home/wj/hadoop-2.7.7/etc/hadoop/h ...

  6. Antd组件库使用方法

    零.介绍: Ant design,是阿里巴巴的蚂蚁金服公司设计的一套适应用于web端和移动端网页的Ui组件库,组件好看,非常适合React框架使用. 官网:https://ant.design/ind ...

  7. C语言之函数用法总结

    C语言函数概述: 构成C语言程序的基本模块,模块化编程的最小单位. 函数调用的基本方式: 函数调用时的数据传递: 函数调用的过程: 函数原型与函数定义的区别: 函数封装: 1.外界对函数的影响仅限于入 ...

  8. 网络、TCP协议与UDP协议

    1.网络模型 (1)什么是网络模型 网络编程的本质是两个设备之间的数据交换,当然,在计算机网络中,设备主要指计算机.数据传递本身没有多大的难度,不就是把一个设备中的数据发送给两外一个设备,然后接受另外 ...

  9. java实现阿里云短信服务发送验证码

    由于做项目的时候遇到了接第三方短信服务,所以记录一下. 一.新建一个maven项目并导入相关依赖 <!--手机发送短信验证码--> <dependency> <group ...

  10. Your Ride Is Here 你的飞碟在这儿 USACO 模拟

    1001: 1.1.1 Your Ride Is Here 你的飞碟在这儿 时间限制: 1 Sec  内存限制: 128 MB提交: 9  解决: 9[提交] [状态] [讨论版] [命题人:外部导入 ...