目录
一、引用类型和基本类型的区别
在javaScript中,可以分为两种类型:基本类型和复杂类型
两种类型的区别:存储位置不同
1.基本类型
基本类型主要为6种:
- Number
- String
- Boolean
- Undefined
- null
- symbol
1.1 Number
数值最常见的整数类型格式则为十进制,还可以设置八进制(零开头),十六进制(0x开头)。
let intNum = 55 // 10进制的55
let num1 = 070 // 8进制的56
let hexNum = 0xA // 16进制的10
浮点类型则在数值汇总必须包含小数点,还可以通过科学计算法表示
let floatNum1 = 1.1
let floatNum2 = 0.1
let floatNum3 = .1 // 有效但不推荐
let floatNum = 3.125e7 // 等于31250000
在数值类型中,存在一个特殊数值NaN,表示“不是数值”,用于表示本来要返回数值的操作失败了(而不是抛出错误)
console.log(0/0)
// NaN
console.log(-0/+0)
// NaN
1.2 Undefined
Undefined类型只有一个值,就是特殊值undefined。当使用var或者let声明了变量但没有初始化时,就相当于给变量赋予了undefined值。
let message
console.log(message == undefined) // true
包含undefined值的变量跟未定义变量是有区别的
message这个变量被声明了,只是值为undefined
没有声明age这个变量,就会报错
1.3 String
字符串可以使用双引号(“”)、单引号(‘’)或者反引号(``)标示
字符串是不可变的,意思是一旦创建了,它们的值就不能变了
// 可以先销毁再创建
1.4 Null
Null类型只有一个值,即特殊值null。
逻辑上讲,null值表示一个空对象指针,这也是给typeof传一个null会返回’object‘的原因。
undefined值是由null值派生而来。
只要变量要保存对象,而当时又没有那个对象可保存,就可以用ull来填充该变量。
1.5 Boolean
Boolean布尔值类型有两个字面值:true和false。
通过Boolean可以将其他类型的数据转化成布尔值。
规则如下:
数据类型 | 转换为true的值 | 转化为false的值 |
---|---|---|
String | 非空字符串 | “ ” |
Number | 非零数值(包括无穷值) | 0、NaN |
Object | 任意对象 | null |
Undefined | N/A(不存在的) | undefined |
1.6 Symbol
Symbol(符号)是原始值,且符号实例时唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。
let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false
let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false
2.引用类型
复杂类型统称为引用类型Object,主要描述下面三种:
- Object
- Array
- Function
2.1 Object
创建object常用方式为对象那个字面量表示法,属性名可以是字符串或数值。
2.2 Array
javaScript数组是一组有序的数据,但跟其他语言不同的是,数组中每个槽位可以存储任意类型的数据。并且,数组也是有动态大小的,会随着数据添加而自动增长。
2.3 Function
函数实际上是对象,每个函数都是Function类型的实例,而Function也有属性和方法,跟其他引用类型一样。
函数存在三种常见的表达方式:
- 函数声明
- 函数表达式
- 箭头函数
函数声明和函数表达式两种方式
其他引用类型
除上述三种方式以外,还包括Date、RegExp、Map、Set等......
3.存储区别
基本数据类型和引用数据类型存储在内存中的不同位置:
- 基本数据类型存储在栈中
- 引用数据类型存储在堆中
当把变量赋给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。
3.1 基本类型
a的值为基本类型,是存储在栈中,将a的值赋给b,虽然两个变量的值相等,但是两个变量保存在了不同的内存地址。
基本类型赋值过程:
栈内存
3.2 引用类型
var obj1 = {}
var obj2 = obj1;
obj2.name = "Xxx";
console.log(obj1.name); // xxx
引用类型数据存放在堆内存中,每个堆内存中有一个引用地址,该引用地址存放在栈中。
obj1是一个引用类型,在赋值操作过程中汇总,实际将堆内存对象在栈内存的引用地址复制了一份给了obj2,实际上他们共同指向同一个堆内存对象,所以更改obj2会对obj1产生影响。
引用类型赋值过程:
4.总结
4.1 声明变量时不同的内存地址分配:
- 简单类型的值存放在栈中,在栈中存放的是对应的值
- 引用类型对应的值存储在堆中,在栈中存放的是指向堆内存的地址
4.2 不同的类型数据导致赋值变量时不同:
- 简单类型赋值,是生成相同的值,两个对象对应不同的地址
- 引用类型赋值,是将保存在对象的内存地址赋值给另一个变量。也就是两个变量指向堆内存中同一个对象
二、== 和===区别,在什么情况下使用?
1.等于操作符( == )
等于操作符用两个等于号( == )表示,如果操作数相等,则会返回true。
JavaScript中存在隐式转换,等于操作符( == )在比较中会进行类型转换,再确定操作数是否相等。
- 如果任意操作数是布尔值,则会将其转换为数值再比较是否相等。
- 如果一个操作数是字符串,另一个操作数是数值,则尝试将字符串转换为数值,再比较是否相等。
- 如果一个操作数是对象,另一个操作数不是,则调用对象的valueOf( )方法取得其原始值,再根据前面的规则进行比较。
let obj = {valueOf:function(){return 1}}
let result1 = (obj == 1); // true
- null和undefined相等。
- 如果任意操作符是NaN,则相等操作符返回false 。
- 如果两个操作符都是对象,则比较它们是不是同一个对象,如果两个操作符都指向同一个对象,则相等操作符返回true。
let obj1 = {name:"xxx"}
let obj2 = {name:"xxx"}
let result1 = (obj1 == obj2 ); // false
总结:
- 两个都是简单类型,字符串和布尔值都会转换成数值,再比较
- 简单类型与引用类型比较,对象转化成其原始类型的值,再比较
- 两个都为引用类型,则比较它们是否指向同一个对象
- null和undefined相等
- 存在NaN则返回false
2.全等操作符
全等操作符是由(===)表示,只有两个操作数在不转换的前提下相等才会返回true。即类型相同,值相同。
不相等是因为数据类型不同 。
相等是因为数据类型相同,值相等。
undefined和null与自身严格相等。
3.区别
相等操作符(==)会做类型转换,再进行值的比较,全等运算符(===)不会做类型转换。
null和undefined比较,相等操作符(==)为true,全等为false。
4.总结
相等操作符隐藏的类型转换,会带来一些违反直觉的结果。
但在比较null的情况下,一般使用相等操作符(==)。
const obj = {}
if(obj.n == null){
console.log("1") // 执行
}
等同于以下写法:
使用相等操作符(==)的写法更加简洁。
除了在比较对象属性为null或者undefined的情况下,可以使用相等操作符(==)。
其他情况一律使用全等操作符(===)。
三、typeof与instanceof区别
1.typeof
typeof操作符返回一个字符串,表示未经计算的操作数的类型。
使用方法:
operand表示对象或原始值的表达式,其类型将被返回。
以上例子前6个都是基础数据类型,虽然typeof null为objec,但这只是JavaScript存在的一个悠久的bug,不代表null就是引用数据类型,并且null本身也不是对象。
null在typeof之后返回的是有问题的结果,不能作为判断null的方法,如果需要在if语句中判断是否为null,直接通过 ===null 来判断即可。
可以发现引用类型数据,用typeof来判断,除了function会被识别出来,其余的都输出object。
判断一个变量是否存在,可以使用typeof:(不能使用if(a),若a未声明,会报错)。
2.instanceof
instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。
object instanceof constructor
object为实例对象,constructor为构造函数。
构造函数通过new可以实例化对象,instanceof能判断这个对象是否是之前那个构造函数生成的对象。
// 定义构造函数
let car = function()[]
let ben = new car()
ben instanceof car // true
let car = new String('zz')
car instanceof String
let str = 'zz'
str instanceof String
关于instanceof的实现原理:
也就是顺着原型链去找,直到找到相同的原型对象,返回true,否则返回false。
function myInstanceof(left,right) {
//这里先用typeof来判断基础数据类型,如果是,直接返回false
if(typeof left !== 'object' || left === null) return false
// getProtypeof是object对象自带的API,能够拿到参数的原型对象
let proto = Object.getPrototypeOf(left)
while(true) {
if(proto === null) return false
if(proto === right.prototype) return true
//找到相同原型对象,返回true
proto = Object.getPrototypeOf(proto)
}
}
3.区别
typeof和instanceof都是判断数据类型的方法,区别:
- typeof会返回一个变量的基本类型,instanceof返回的是一个布尔值
- instanceof可以准确的判断复杂引用数据类型,但是不能正确判断基础数据类型
- typeof也存在弊端,它虽然可以判断基础数据类型(null除外),但是引用数据类型中,除了function类型以外,其他无法判断。
两者都有弊端,不能满足所有场景的需求。
需要通过检测数据类型,可以采用Object.prototype.toString,调用该方法,统一返回格式”[object Xxx]“的字符串。
Object.prototype.toString的基本用法:
全局通用的数据类型判断方法:
function getType(obj){
let type = typeof obj
// 先进行typeof判断,如果是基础数据类型,直接返回
if (type !== "object") {
return type
}
// 对于typeof返回结果是object的,在进行如下的判断,正则返回结果
return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/,'$1')
}
使用如下:
四、深拷贝浅拷贝的区别?如何实现一个深拷贝?
浅拷贝:
拷贝基本数据类型为其他的值,拷贝引用数据类型为地址,生成新的数据,修改新的数据会影响原数据,实际开发常用的方法有:
- object.assgin
- 扩展运算符
- 等等
深拷贝:
在内存中开辟一个新的栈空间保存新的数据,修改新数据不会影响到原数据,开发中常用的方法有:
- loadsh中的_.cloneDeep()方法
- JSON.stringify()
1.数据类型存储
JavaScript中存在两大数据类型:
- 基本类型
- 引用类型
基本类型数据保存在栈内存中。
引用类型数据保存在堆内存中,引用数据类型的变量是一个指向堆内存中实际对象的引用,存在栈中。
2.浅拷贝
指的是创新数据,这个数据有着原始数据属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址。
浅拷贝的实现:
在JavaScript中,存在浅拷贝的现象有:
- object.assign
- Array.prototype.slice()
- Array.prototype.concat()
- 使用扩展运算符实现的复制
2.1 object.assign
2.2 slice()
2.3 concat()
2.4 扩展运算符
3.深拷贝
深拷贝开辟一个新的栈,两个对象属性完全相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性。
常见的深拷贝有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
3.1 _cloneDeep()
const _ = require('lodash')
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3],
}
const obj2 = _.cloneDeep(obj1)
console.log(obj1.b.f === obj2.b.f) // false
3.2 jQuery.extend()
const $ = require('jquery')
const obj1 = {
a: 1,
b: { f: { g: 1 } },
c: [1, 2, 3],
}
const obj2 = $.extend(true, {}, obj1)
console.log(obj1.b.f === obj2.b.f) // false
3.3 JSON.stringify()
const obj2 = JSON.parse(JSON.stringify(obj1))
这个方式存在弊端,会忽略undefined、symbol和函数。
const obj = {
name: 'A',
name1: undefined,
name3: function () {},
name4: Symbol('A'),
}
const obj2 = JSON.parse(JSON.stringify(obj))
console.log(obj2) // {name: "A"}
3.4 循环递归
function deepClone(obj, hash = new WeakMap()) {
if (obj === null) return obj // 如果是null或者undefined我就不进行拷贝操作
if (obj instanceof Date) return new Date(obj)
if (obj instanceof RegExp) return new RegExp(obj)
// 可能是对象或者普通的值 如果是函数的话是不需要深拷贝
if (typeof obj !== 'object') return obj
// 是对象的话就要进行深拷贝
if (hash.get(obj)) return hash.get(obj)
let cloneObj = new obj.constructor()
// 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
hash.set(obj, cloneObj)
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
// 实现一个递归拷贝
cloneObj[key] = deepClone(obj[key], hash)
}
}
return cloneObj
}
4.区别
深拷贝和浅拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样。
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象。
深拷贝会另外创造一个一摸一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
// 深拷贝
const obj1 = {
name: 'init',
arr: [1, [2, 3], 4],
}
const obj4 = deepClone(obj1) // 一个深拷贝方法
obj4.name = 'update'
obj4.arr[1] = [5, 6, 7] // 新对象跟原对象不共享内存
console.log('obj1', obj1) // obj1 { name: 'init', arr: [ 1, [ 2, 3 ], 4 ] }
console.log('obj4', obj4) // obj4 { name: 'update', arr: [ 1, [ 5, 6, 7 ], 4 ] }
5.总结
前提为拷贝类型为引用类型的情况下:
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址。
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝时新开栈,两个对象指向不同的地址。
二、对作用域链的理解?
简单回答即:
作用域一般可以理解为函数或变量的生效范围,我们一般把作用域分成全局作用域,函数(局部)作用域,块级作用域(es6推出),例如:
我们在a函数中定义了一个变量,那么当我们在js中访问这个变量他就会在当前作用域进行查找,如果访问不到,他会一层一层向外进行查找,整个逐级向上查找的过程我们称为作用域链。
1.作用域
作用域即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合。
即,作用域决定了代码区块中变量和其他资源的可见性。
例:
// 要先执行obj()这个函数,否则根本不知道里面是啥
函数obj内部创建一个obj1变量,当我们在全局访问这个变量的时候,系统就会报错,说明我们在全局是无法获取到(闭包除外)函数内部的变量。
作用域分为:
- 全局作用域
- 函数作用域
- 块级作用域
1.1 全局作用域
任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。
1.2 函数作用域
函数作用域也叫做局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问。
上述代码中在函数内部声明的变量或函数,在函数外部是无法访问的,这说明在函数内部定义的变量或者方法只是函数作用域。
1.3 块级作用域
ES6中引入了let 和const 关键字,和var关键字不同的,在大括号中使用let和const声明的变量存在于块级作用域中,在大括号之外不能访问这些变量。
2.词法作用域
词法作用域又叫做静态作用域,变量被创建时就确定好了,而非执行阶段确定的,也就是我们写好代码时它的作用域就确定了,JavaScript遵循的就是词法作用域。
相同层级的goo和oog就没有办法访问到彼此块级作用域中的变量,所以输出1。
3.作用域链
当在JavaScript中使用一个变量的时候,首先JavaScript引擎会尝试在当前作用域下去寻找该变量,如果没有找到,再找它的上层作用域寻找,以此类推直到找到该变量或是已经到了全局作用域。
如果在全局作用域里仍然找不到该变量,它就会在全局范围内隐式声明该变量(非严格模式下)或是直接报错。
在《你不知道的JavaScript(上)》中:把作用域比喻成一个建筑,这份建筑代表程序中的嵌套作用域链,第一层代表当前的执行作用域,顶层代表全局作用域,变量的引用会顺着当前楼层进行查找,如果找不到,则会往上一层找,一旦到达顶层,查找的过程都会停止。
代码展示:
- student函数内部属于最内层作用域,找不到name,向上一层作用域person函数内部找,找到了输出’张'。
- student内部输出sex时找不到,向上一层作用域person函数找,还找不到继续向上一层找,即全局作用域,找到了输出‘女’。
- 在person函数内部输出age时找不到,向上一层作用域找,即全局作用域,还是找不到就会报错。
三、箭头函数
箭头函数是ES6新增的特性,用来简化普通函数的写法,规避普通函数this指向的痛点。
1.箭头函数没有原型对象prototype,不能作为构造函数使用(不能被new)。
2.箭头函数没有arguments,可以使用`...`拿到所有实参的集合数组。
3.箭头函数中的this在定义时就已经确定,取决于父级的环境。
4.箭头函数不能通过call、apply、bind方法修改它的this指向(会忽略第一个参数,其他功能还是可以正常使用)。
5.箭头函数不能用作Generator函数,不能使用yeild关键字(function\*)
总结:
箭头函数时定义函数一种新的方式,他比传统函数function定义更加方便和简单,他没有绑定自己的this指向和伪数组arguments,无法调用super方法生成实例化对象,因为他不是构造函数,一般用来取代匿名函数的写法,最主要的是箭头函数的this指向它的上一级作用域中的this也可以理解为它的this是固定的,而普通函数的this是可变的。