林大妈的JavaScript进阶知识(一):对象与内存
JavaScript中的基本数据类型
在JS中,有6种基本数据类型:
- string
- number
- boolean
- null
- undefined
- 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):
- 普通函数调用 —— this指向全局
- 方法调用 —— this指向方法所定义的对象
- 构造器调用 ——(使用new关键字时)this指向当前函数对象(函数本身就是对象)
- (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进阶知识(一):对象与内存的更多相关文章
- 林大妈的JavaScript进阶知识(二):JS异步行为
JavaScript 是单线程执行的 JavaScript运行在浏览器中.浏览器是多线程的,但只分配了其中一条给JavaScript,作为它的主线程.对于编码者来说,JavaScript是单线程的.因 ...
- 林大妈的JavaScript进阶知识(三):HTML5 History API
HTML5中新增了History API,它用于管理浏览器路由跳转的一个url栈.History是window对象的一部分,它也是一个对象,因此称它是BOM(类似DOM,Browser Object ...
- 林大妈的JavaScript基础知识(三):JavaScript编程(2)函数
JavaScript是一门函数式的面向对象编程语言.了解函数将会是了解对象创建和操作.原型及原型方法.模块化编程等的重要基础.函数包含一组语句,它的主要功能是代码复用.隐藏信息和组合调用.我们编程就是 ...
- 林大妈的JavaScript基础知识(三):JavaScript编程(3)原型
在一般的编程语言中,我们使用继承来复用代码,做成良好的数据结构.而在JavaScript中,我们使用原型来实现以上的需求.由于JavaScript专注于对象而摒弃了类,我们要明白原型和继承的确是有差异 ...
- 林大妈的JavaScript基础知识(三):JavaScript编程(4)数组
数组,是一段线性分配的,具有非常高性能的数据结构.简单地说,数组以连续的空间存储,通过整数地计算偏移量访问其中的元素,将读取修改的时间复杂度降低至O(1),我们称之为猝发式存取.是不是非常期待?没错, ...
- 林大妈的JavaScript基础知识(三):JavaScript编程(1)对象
1. 对象的简单介绍与一些注意事项 JavaScript中具有几个简单数据类型:数字.字符串.布尔值.null值以及undefined值.除此之外其余所有值(包括数组.函数,甚至正则表达式)都是对象. ...
- 林大妈的JavaScript基础知识(一):JavaScript简史
前言:做一名Web设计师是一件令人兴奋的事.在Web技术中,JavaScript是一个经历从被人误解到万众瞩目的巨大转变,在历史的冲击中被留存下来的个体.因为JavaScript的引导,Web开发也从 ...
- 林大妈的JavaScript基础知识(二):编写JavaScript代码前的一些简单工作
在介绍JavaScript语法前,我们需要知道,学习语法必须要多利用手敲代码来巩固记忆.因此,由于JavaScript的特性,它不能像C++和Java一样独立地编译及运行,我们需要在调试运行JavaS ...
- 二、JavaScript语言--JS基础--JavaScript进阶篇--DOM对象 控制HTML元素
1.认识DOM 文档对象模型DOM(Document Object Model)定义访问和处理HTML文档的标准方法.DOM 将HTML文档呈现为带有元素.属性和文本的树结构(节点树). 先来看看下面 ...
随机推荐
- 1061 判断题 (15 分)C语言
判断题的评判很简单,本题就要求你写个简单的程序帮助老师判题并统计学生们判断题的得分. 输入格式: 输入在第一行给出两个不超过 100 的正整数 N 和 M,分别是学生人数和判断题数量.第二行给出 M ...
- jenkins +git+ssh 构建 .net项目
jenkins +git+ssh 构建 .net项目 安装jenkins jdk 和插件就不一一介绍了. Multiple SCMs 插件介绍:可以获取多个项目(如果你的项目中有依赖其他项目的) So ...
- Ant Design中getFieldDecorator方法的特殊用法(小bug)
记录Ant Design中getFieldDecorator方法的特殊的一个用法 了解Ant Design表单的小伙伴都知道,getFieldDecorator在大部分情况下是用来绑定一个控件的,即像 ...
- HashMap,HashTable 区别,实现原理。
HashMap是HashTable 的轻量级,非线程安全的,都是实现了map接口 区别:hashmap 允许空键值对的存在,非线程安全,效率高于hashtable,因为hashtable 是synch ...
- 从0开发3D引擎(六):函数式反应式编程及其在引擎中的应用
目录 上一篇博文 介绍函数式反应式编程 函数式反应式编程学习资料 函数式反应式编程的优点与缺点 优点 缺点 异步处理的其它方法 为什么使用Most库 引擎中相关的函数式反应式编程知识点 参考资料 大家 ...
- C++ | C++ 基础知识 | 结构、联合与枚举
1. 结构 1.0 结构 数组是相同类型元素的集合,相反,struct 是任意类型元素的集合. 代码例子: struct Address { const char* name; int number; ...
- 字符串转hash进阶版
#include<bits/stdc++.h> using namespace std; ,mod=; vector<unsigned> H[mod]; void Add(un ...
- css文字溢出显示省略号
1.单行文字溢出显示省略号. overflow: hidden; text-overflow: ellipsis; white-space: nowrap;//文本不换行 2.多行文本溢出显示省略号. ...
- React16源码解读:揭秘ReactDOM.render
引言 在上一篇文章中我们通过create-react-app脚手架快速搭建了一个简单的示例,并基于该示例讲解了在类组件中React.Component和React.PureComponent背后的实现 ...
- 三、JVM之方法区
一.什么式方法区 方法区,也称非堆(Non-Heap),又是一个被线程共享的内存区域.其中主要存储加载的类字节码.class/method/field等元数据对象.static-final常量.sta ...