javascript的数据类型,存储上的差别?
在js中,我们可以分为两种类型:
基本数据类型:
- Number:数值最常见的整数类型格式就是十进制,还可以设置八进制,十六进制,在数值类型中,存在一个特殊数值NaN,他是用来表示本来要返回数值的操作失败了
- string:字符串可以使用单引号,双引号,或反引号表示
- Boolean:只有两个字面量,true和false
- Undefined:是一个特殊值,undefined值的变量和未定义变量是有区别的
- null:null类型同样只有一个值,逻辑上讲,null值表示的就是一个空对象指针,只要变量要保存对象,而当时又没有那个对象可保存,就可用
null
来填充该变量 - symbol:Symbol (符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险
复杂数据类型:
- Object:创建object常用的方式为对象字面量表达式,属性名可以是字符串或数值
- Array:
JavaScript
数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据 - Function:函数上就是一个对象,每个函数都是function类型的实例,而function也有属性和方法,跟其他引用类型一样
其他引用类型
date。RegExp,Map,Set等
存储区别:
基本数据类型和引用数据类型存储在内存中的位置不同:
-
基本数据类型存储在栈中
-
引用类型的对象存储于堆中
总结
- 声明变量时不同的内存地址分配:
- 简单类型的值存放在栈中,在栈中存放的是对应的值
- 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
- 不同的类型数据导致赋值变量时的不同:
- 简单类型赋值,是生成相同的值,两个对象对应不同的地址
- 复杂类型赋值,是将保存对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象
数组中常用的方法有哪些?
数组操作的增删改查操作
增
- push() 接收任意数量的参数,添加在数组的末尾,返回数组的最新长度
- unshift() 在开头添加任意多个值,然后返回新的数组的长度
- splice()传入三个参数,分别是开始位置、要删除的元素数量、插入的元素,返回空数组
- concat()会创建一个当前数组的副本,然后再把它的参数添加到副本的末尾,最后返回这个新构建的数组,不会影响原始数组
删
- pop() 用于删除数组的最后一项,同时会减少数组的length,返回被删除的项
- shift()用于删除数组的第一项,同时减少数组的length,返回被删除的项
- splice()传入两个参数,分别是开始位置,删除元素的数量,返回包含删除元素的数组
- slice()用于创建一个包含原有数组中的一个或者多个元素 ,不会影响原始数组
改
splice():接收三个参数,分别是开始位置,要删除元素的数量,要插入的任意多个元素,返回删除元素的数组,对原数组产生影响
查
- indexOf()返回要查找的元素在数组中的位置,如果没有则返回-1
- includes()返回要查找的元素在数组中的位置,找到返回true,否则是false
- find()返回的是第一个匹配的元素
排序方法
- reverse()将数组元素方向反转
- sort()接收一个比较函数,用于判断哪个值应该排在前面
转换方法
join()接收一个参数,即字符串分隔符,返回包含所有项的字符串
数组迭代(都不会改变原数组)
- some()如果至少有一个元素返回true,则方法就返回true
- every()如果所有元素返回true,则这个方法就返回true
- forEach()对数组的每一项都运行传入的函数,没有返回值
- filter()对数组进行过滤,函数返回true的项会组成一个数组后进行返回
- map()对数组中的每一项都进行遍历,在react中使用较多
javascript字符串常用的方法有哪些?
增
这里增的意思不是说直接增添内容,而是创建字符串的副本,再进行操作
除了我们常用的+以及${]进行字符串拼接外,还可以通过concat
concat()用于将一个或者多个字符串拼接为一个新的字符串
删
- slice()
- substr()
- substring()
这三个方法都返回调用它们的字符串的一个子字符串,而且都接收一或两个参数
改
-
trim()、trimLeft()、trimRight()删除前、后或前后所有空格符,再返回新的字符串
-
repeat()接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
-
padStart()、padEnd()复制字符串,如果小于指定长度,则在相应一边填充字符,直至满足长度条件
-
toLowerCase()、 toUpperCase()大小写转化
查
-
chatAt()返回给定索引位置的字符,由传给方法的整数参数指定
-
indexOf()从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
-
startWith()从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
-
includes()从字符串中搜索传入的字符串,并返回一个表示是否包含的布尔值
转换方法
split()把字符串按照指定的分割符,拆分成数组中的每一项
模板匹配
- match()接收一个参数,可以是一个正则表达式字符串,也可以是一个
RegExp
对象,返回数组 - search()接收一个参数,可以是一个正则表达式字符串,也可以是一个
RegExp
对象,找到则返回匹配索引,否则返回 -1 - replace()接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
javascript中的类型转换机制?
常见的类型转换有:
强制转换(显示转换)
- Number()将任意类型的值转化为数值
- parseInt()
parseInt
相比Number
,就没那么严格了,parseInt
函数逐个解析字符,遇到不能转换的字符就停下来 - String()可以将任意类型的值转化成字符串
- Boolean()可以将任意类型的值转为布尔值
自动转换 (隐式转换)
何时会发生隐式转换?
我们这里可以归纳为两种情况发生隐式转换的场景:
- 比较运算(
==
、!=
、>
、<
)、if
、while
需要布尔值地方 - 算术运算(
+
、-
、*
、/
、%
)
除了上面的场景,还要求运算符两边的操作数不是同一类型
自动转换为布尔值
在需要布尔值的地方,就会将非布尔值的参数自动转为布尔值,系统内部会调用Boolean
函数
可以得出除了undefined,null,false,+0,-0,NaN,""会被转换为false,其他的会被转换为true
自动转换为字符串
遇到预期为字符串的地方,就会将非字符串的值自动转为字符串
具体规则是:先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串
常发生在+
运算中,一旦存在字符串,则会进行字符串拼接操作
自动转换为数值
除了+有可能把运算转为字符串,其他的运算符都会把运算自动转成数值
null
转为数值时,值为0
undefined
转为数值时,值为NaN
==和===的区别?分别在什么情况下使用
==
如果操作数相等,则会返回true,在比较中会先进行类型转换,再确定操作数是否相等
遵循以下规则:
-
两个都为简单类型,字符串和布尔值都会转换成数值,再比较
-
简单类型与引用类型比较,对象转化成其原始类型的值,再比较
-
两个都为引用类型,则比较它们是否指向同一个对象
-
null 和 undefined 相等
-
存在 NaN 则返回 false
全等操作符(===)
全等操作符由 3 个等于号( === )表示,只有两个操作数在不转换的前提下相等才返回 true
。即类型相同,值也需相同
undefined
和 null
与自身严格相等
区别?
相等操作符(==)会做类型转换,再进行值的比较,全等运算符不会做类型转换
null
和 undefined
比较,相等操作符(==)为true
,全等为false
总结:
除了在比较对象属性为null
或者undefined
的情况下,我们可以使用相等操作符(==),其他情况建议一律使用全等操作符(===)
深拷贝浅拷贝的区别?如何实现一个深拷贝?
深拷贝:值的是创建一个新的数据,这个数据有着原始数据属性值的一份精准拷贝
如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址
即浅拷贝只是拷贝一层,深层次的引用类型则是共享内存地址
浅拷贝的实现:
Object.assign
Array.prototype.slice()
,Array.prototype.concat()
- 使用拓展运算符实现的复制
深拷贝
深拷贝开辟了一个新的栈,两个对象属性完全相同,但是对应了两个不同的地址,修改一个对象的属性,不会该白你另一个对象的属性
常见的深拷贝的方式有:
-
_.cloneDeep()
-
jQuery.extend()
-
JSON.stringify()
-
手写循环递归
区别?
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是会共享同一块内存,修改对象属性会影响原对象
深拷贝会创造出一个一摸一样的对象,新旧对象不共享内存,修改新对象不会影响到原对象
谈谈你对闭包的理解?以及使用的场景?
函数嵌套函数,内部函数始终可以访问到其外部函数的作用域以及参数,这就是闭包
在js中,我们每创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁
使用场景:
创建私有变量
延长变量的生命周期
一般函数的词法环境在函数返回后就被销毁,但是闭包会保存对创建时所在词法环境的引用,即便创建时所在的执行上下文被销毁,但是创建时所在词法环境仍然存在,以达到延长变量生命周期的目的
柯里化函数
柯里化的目的在于避免频繁的调用具有相同参数函数的同时,又能轻松的重用
注意事项:
如果不是某些特定任务需要使用闭包,在其他函数中创建函数是不明智的,因为闭包在处理速度和内存消耗方面对脚本性能具有负面影响
谈谈你对作用域链的理解?
作用域,即变量和函数生效的区域或集合,作用域决定了代码块中变量和其他资源的可见性
我们一般将作用域分为三种:
全局作用域:任何不再函数中或者是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问
函数作用域:函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面,这些变量只能在函数内部访问,不能在函数以外进行访问
块级作用域:像我们es6中引入了let和const关键字,let和const属于块级作用域,在大括号以外不能访问这些变量
词法作用域:
又叫静态作用域,变量被创建时确定好了,而非执行阶段确定的,也就是说我们写好的代码时他的作用域就确定了,js遵循的就是词法作用域
作用域链:
在js中使用一个变量的时候,首先js引擎会尝试在当前作用域下去寻找该变量,如果没有找到变量,会到他的上层作用域寻找,以此类推直到找到该变量或者到了全局作用域
如果在全局作用域仍然找不到该变量,他就会在全局范围内隐式声明该变量
JavaScript原型,原型链 ? 有什么特点?
js常被描述为一个基于原型的语言--每个对象拥有一个原型对象
当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型,以及该对象的原型的原型,依次向上搜索,直到找到一个名字匹配的属性或者达到原型链的顶端
原型链:
原型对象也可能拥有原型,并从中继承方法和属性,一层一层,以此类推,这种关系被称为原型链,他解释了为何一个对象会拥有定义在其他对象中的属性和方法
在对象实例和他的构造器之间建立一个链接,_proto_属性,是从构造函数的prototype属性中派生的,之后可以通过上溯原型链,在构造器中找到这些属性和方法
总结:
一起对象都是继承自object对象,object对象直接继承根源对象null
一切的函数对象,都是继承自Function对象
object对象直接继承自Function对象
Function对象的_proto_会指向自己的原型对象,最总还是继承自object对象
Javascript如何实现继承?
继承:是面向对象软件技术中的一个概念
如果一个类别B“继承自”另一个类别A,就把这个B称为“A的子类”,而把A称为“B的父类别”也可以称“A是B的超类”
继承的优点:
继承可以使得子类具有父类别的各种属性和方法,而不需要再次编写相同的代码
再子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得于父类别不同的功能
实现方式:
-
原型链继承:是比较常见的一种继承方式之一,其中设计的构造函数、原型和实例,三者之间存在一定的关系,即每一个构造函数中都会有一个原型对象,原型对象有包含一个指向构造函数的指针,而实例则包含一个原型对象的指针
-
构造函数继承(借助 call)借助call调用Parent函数,父类原型对象中一旦存在父类之前自己定义的方法,那么子类将无法继承这些方法,父类的引用属性不会被共享,优化了原型链继承的弊端,但是它只能继承父类的实例属性和方法,不能继承原型属性或者方法
-
组合继承:组合继承就是将原型链继承和构造函数继承结合起来
-
原型式继承:借助Object.create方法实现普通对象的继承:因为它实现的是浅拷贝,多个实例的引用类型属性指向相同的内存,存在篡改的可能
-
寄生式继承:就是利用浅拷贝的能力进行增强,添加一些其他方法
-
寄生组合式继承:借助解决普通对象的继承问题的
Object.create
方法,在前面几种继承方式的优缺点基础上进行改造,这也是所有继承方式里面相对最优的继承方式
谈谈this对象的理解?
函数的this关键字在js中表现得略有不同,在严格模式下和非严格模式下也会有一些差别
在绝大多数情况下,函数的调用方式决定了this的值
this关键字是在函数运行时自动生成一个内部对象,只能在函数内部使用,总能调用他的对象
在this的执行过程中,一旦被确定,就不能再进行更改
绑定规则:
-
默认绑定:全局环境中定义
person
函数,内部使用this
关键字 -
隐式绑定:函数还可以作为某个对象的方法调用,这时
this
就指这个上级对象 -
new绑定:通过构建函数
new
关键字生成一个实例对象,此时this
指向这个实例对象 -
显示绑定:
apply()、call()、bind()
是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this
指的就是这第一个参数
箭头函数
es6中还提供了箭头函数语法,让我们在代码书写时就能确定this指向
虽然箭头函数的this能够在编译的时候就确定this的指向,但是我们也需要注意一些潜在的坑
箭头函数没有this指向,箭头函数不能作为构造函数
优先级
new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级
JavaScript中执行上下文和执行栈是什么?
简单来说,执行上下文是一种对js代码执行环境的抽象概念,也就是说只有js代码运行,那么它就一定是运行在执行的上下文中
执行上下文的类型分为三种:
- 全局执行上下文:只有一个,浏览器中的全局对象就是
window
对象,this
指向这个全局对象 - 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
- Eval 函数执行上下文: 指的是运行在
eval
函数中的代码,很少用而且不建议使用
生命周期:
执行上下文的生命周期包括三个阶段:创建阶段 → 执行阶段 → 回收阶段
创建阶段:
创建阶段做了三件事:
- 确定 this 的值,也被称为
This Binding
- LexicalEnvironment(词法环境) 组件被创建
- VariableEnvironment(变量环境) 组件被创建
词法环境
词法环境有两个组成部分:
-
全局环境:是一个没有外部环境的词法环境,其外部环境引用为
null
,有一个全局对象,this
的值指向这个全局对象 -
函数环境:用户在函数中定义的变量被存储在环境记录中,包含了
arguments
对象,外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境
变量环境
变量环境也是一个词法环境,因此它具有上面定义的词法环境的所有属性
在 ES6 中,词法环境和变量环境的区别在于前者用于存储函数声明和变量( let
和 const
)绑定,而后者仅用于存储变量( var
)绑定
执行阶段
在这阶段,执行变量赋值、代码执行
如果 Javascript
引擎在源代码中声明的实际位置找不到变量的值,那么将为其分配 undefined
值
回收阶段
执行上下文出栈等待虚拟机回收执行上下文
说说JavaScript中的事件模型
javascript
中的事件,可以理解就是在HTML
文档或者浏览器中发生的一种交互操作,使得网页具备互动性, 常见的有加载事件、鼠标事件、自定义事件等
事件流都会经历三个阶段:
- 事件捕获阶段(capture phase)
- 处于目标阶段(target phase)
- 事件冒泡阶段(bubbling phase)
事件冒泡是一种从下往上的传播方式,由最具体的元素(触发节点)然后逐渐向上传播到最不具体的那个节点,也就是DOM
中最高层的父节点
事件捕获与事件冒泡相反,事件最开始由不太具体的节点最早接受事件, 而最具体的节点(触发节点)最后接受事件
事件模型可以分为三种:
原始事件模型(DOM0级)html代码直接绑定,js代码绑定
特性:
绑定速度快
只支持冒泡,不支持捕获
同一个类型的事件只能绑定一次
标准事件模型(DOM2级)
- 事件捕获阶段:事件从
document
一直向下传播到目标元素, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行 - 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数
- 事件冒泡阶段:事件从目标元素冒泡到
document
, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行
特性:
- 可以在一个
DOM
元素上绑定多个事件处理器,各自并不会冲突当第三个参数(
useCapture
)设置为true
就在捕获过程中执行,反之在冒泡过程中执行处理函数
IE事件模型(基本不用
IE事件模型共有两个过程:
- 事件处理阶段:事件到达目标元素, 触发目标元素的监听函数。
- 事件冒泡阶段:事件从目标元素冒泡到
document
, 依次检查经过的节点是否绑定了事件监听函数,如果有则执行