数据类型

js共有8种数据类型,分别是Undefined,Null,Boolean,Number,String,Object,Symbol,Bignt。
其中Symbol,Bight是ES6中新增的数据类型:

  • Symbol代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • Bigint 是一种数字类型的数据,它可以表示任意精度格式的证书,使用BigInt可以安全地存储和操作大整数,即使这个数已经超出Number能够表示地安全整数范围。

这些数据乐意分为原始数据类型和引用数据类型:

  • 栈:原始数据类型直接存储在栈中地简单数据段,占据空间小,大小固定,属于被频繁使用数据,所以放入栈中存储。
  • 堆:引用数据类型(对象,数组,函数)

两种类型地区别在于存储位置的不同:

  • 原始数据类型直接存储在栈中的简单数据段,占据空间小,大小固定 ,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆中的对象,占据空间大,大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其中在栈中地额地址,武德地址后从堆中获得实体。

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

  • 在数据结构中,中数据的存取方式为先进先出
  • 是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

在操作系统中,内存被分为栈区和堆区:

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开饭分配释放,若开发者不是释放,程序结束时可能由垃圾回收机制回收。

  1. typeof:其中数组,对象,null都会被判断为object,其它判断都正确。
  2. instanceof可以正确判断对象的类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
1
2
3
4
5
6
7
8
9
10
11
12
console.log(2 instanceof Number)//false
console.log(true instanceof Boolean)//false
console.log('str' instanceof String);//false
//以检测字符串类型false解释原理
//上方放回false,是因为'str'本身不是任何一个类的实例,要想检测的结果为false,只能通过new一个String达到目标
var str=new String('abc')
console.log(str instanceof String)//true
//通过typeof str 检测到str返回的仍是string并不是Object.

console.log([] instanceof Array);//true
console.log(function(){} instanceof Function);//true
console.log({} instanceof object);//true

从上可以看出instanceof只能正确判断引用数据类型,而不能判断基本数据类型。instanceof运算符可以用来测试一个对象在其原型链中是否存在一个构造函数的prototype属性

  3. constructor
1
2
3
4
5
6
console.log((2).constructor === Number); // true
console.log((true).constructor === Boolean); // true
console.log(('str').constructor === String); // true
console.log(([]).constructor === Array); // true
console.log((function() {}).constructor === Function); // true
console.log(({}).constructor === Object); // true

constructor 有两个作用,一是判断数据的类型,二是对象实例通过constrcutor对象访问它的构造函数。需要注意,如果创建一个对象来改变它的原型,constructor就不能用来判断数据类型了。

1
2
3
4
5
6
7
8
function Fn(){};

Fn.prototype = new Array();

var f = new Fn();

console.log(f.constructor===Fn); // false
console.log(f.constructor===Array); // true
  4. Object.prototype.toString.call()

Object.prototype.toString.call() 使用 Object 对象的原型方法 toString 来判断数据类型:

1
2
3
4
5
6
7
8
9
10
var a = Object.prototype.toString;

console.log(a.call(2));
console.log(a.call(true));
console.log(a.call('str'));
console.log(a.call([]));
console.log(a.call(function(){}));
console.log(a.call({}));
console.log(a.call(undefined));
console.log(a.call(null));

同样是检测对象obj调用toString方法,obj.toString()的结果和Object.prototype.toString.call(obj)的结果不一样,这是为什么?
这是因为toString是Object的原型方法,而Array、function等类型作为Object的实例,都重写了toString方法。不同的对象类型调用toString方法时,根据原型链的知识,调用的是对应的重写之后的toString方法(function类型返回内容为函数体的字符串,Array类型返回元素组成的字符串…),而不会去调用Object上原型toString方法(返回对象的具体类型),所以采用obj.toString()不能得到其对象类型,只能将obj转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用Object原型上的toString方法。

首先Undefined和Null都是基本数据类型,这两个基本数据类型分别都只有一个值,就是undefined和null。
undefined代表的含义是未定义,null代表的含义是空对象。一般变量声明了但还没有定义的时候就会返回undefined,null主要用于赋值给一些可能返回对象的变量,作为初始化。
当对这两种类型使用 typeof 进行判断时,Null 类型化会返回 “object”,这是一个历史遗留的问题。当使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。

instanceof 运算符用于判断构造函数的prototype属性是否出现在对象原型链中的任何位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
function myInstanceof(left,right){
//获取对象的原型
let proto=object.getPrototypeOf(left)
//获取构造函数的prototyped对象
let prototype = right.prototype;
//判断构造函数的prototype对象是否在对象的原型链上
while(true){
if(!proto) return false
if(proto === prototype) return true
// 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
proto = Object.getPrototypeOf(proto);
}
}

  • 使用==进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后在进行比较。
  • 使用===进行相等判断时,如果两边的类型不一致,则不会做强制类型转换,直接返回false
  • 使用Object.js来进行相等判断时,一般情况下和===的判断相同,它处理了一些特殊的情况,比如:-0和+0不再相等,两个NaN是相等的。

  • 函数isNaN接受参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的值都会返回true,因此非数字值传入也会返回true,会影响NaN的判断。
  • 函数Number.isNaN会首先判断传入参数是否为数字,如果是数字再继续判断是否为NaN,不会进行数据类型的转换,这种方法对NaN的判断更加准确。

在js中浅拷贝和深拷贝是两种不同的对象拷贝方式。
浅拷贝是指创建一个新对象,然后将原始对象的属性值复制到新对象中。新对象和原始对象共享相同的引用类型属性(如对象,数组等),也就是说,如果修改新对象的引用类型属性,原始对象也会受到影响。
深拷贝是指创建一个新对象,然后递归的将原始对象的所有属性值复制到新对象中。新对象和原始对象不共享引用类型属性,也就是说,修改新对象的引用类型属性不会影响原始对象。
区别:

  • 浅拷贝只复制对象的引用,而不是复制引用指向的值本身;深拷贝则会递归地复制对象及引用的值。
  • 浅拷贝创建的新对象和原始对象共享引用类型属性,而深拷贝创建的新对象和原始对象不共享引用类型属性。

js深拷贝与浅拷贝是什么?区别又是什么
在 JavaScript 中,深拷贝和浅拷贝是两种不同的对象拷贝方式。
浅拷贝是指创建一个新对象,然后将原始对象的属性值复制到新对象中。新对象和原始对象共享相同的引用类型属性(如对象、数组等),也就是说,如果修改新对象的引用类型属性,原始对象也会受到影响。
深拷贝是指创建一个新对象,然后递归地将原始对象的所有属性值复制到新对象中。新对象和原始对象不共享引用类型属性,也就是说,修改新对象的引用类型属性不会影响原始对象。
区别:

  • 浅拷贝只复制对象的引用,而不复制引用指向的值本身;深拷贝则会递归地复制对象及其引用的值。
  • 浅拷贝创建的新对象和原始对象共享引用类型属性,而深拷贝创建的新对象和原始对象不共享引用类型属性。

两种拷贝的实例
以下是两种拷贝方式的实例:

  1. 浅拷贝示例:
1
2
3
4
5
6
7
8
let obj1 = {name: 'Alice', age: 25, hobbies: ['reading', 'painting']};
let obj2 = Object.assign({}, obj1);

obj2.name = 'Bob';
obj2.hobbies.push('running');

console.log(obj1); // {name: 'Alice', age: 25, hobbies: ['reading', 'painting', 'running']}
console.log(obj2); // {name: 'Bob', age: 25, hobbies: ['reading', 'painting', 'running']}

在这个示例中,使用Object.assign()进行浅拷贝。修改了obj2的属性值后,obj1的hobbies属性也被修改了,因为它们共享相同的引用类型属性。
2.深拷贝示例:

1
2
3
4
5
6
7
8
let obj1 = {name: 'Alice', age: 25, hobbies: ['reading', 'painting']};
let obj2 = JSON.parse(JSON.stringify(obj1));

obj2.name = 'Bob';
obj2.hobbies.push('running');

console.log(obj1); // {name: 'Alice', age: 25, hobbies: ['reading', 'painting']}
console.log(obj2); // {name: 'Bob', age: 25, hobbies: ['reading', 'painting', 'running']}

在这个示例中,使用JSON.stringify()将obj1转换为字符串,然后使用JSON.parse()将其转换回对象,从而实现深拷贝。修改了obj2的属性值后,obj1的hobbies属性没有被修改,因为它们不共享引用类型属性。

因为undefined是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响undefined的正常判断。表达式void___没有返回值,因此返回的结果是undefined。void并不改变表达式的结果,只是让表达式不返回值。因此可以用 void 0 来获得undefined。

NaN指”不是一个数字“,NaN是一个”警戒值“,用于指出数字类型中的错误情况,即”执行数学运算没有成功,这是失败后返回的结果“。

1
typeof NaN; //"number"

注意:NaN是一个特殊值,它和本身不相等,是唯一一个非自反(自反:x===x不成立)的值。而NaN!==NaN为true。

扩展运算符:

1
2
3
4
5
6
let outObj={
inObj:{a:1,b:2}
}
let newObj={...outObj}
newObj.inObj.a=2
console.log(outObj)//{inObj:{a:2,b:2}}

Object.assgin():

1
2
3
4
5
6
let outObj={
inObj:{a:1,b:2}
}
let newObj=Object.assign({},outObj)
newObj.inObj.a=2
console.log(outObj)//{inObj:{a:2,b:2}}

从上方实例可以看出两者都属于浅拷贝。

  • Object.assign()方法接受的第一个参数作为目标对象,后面的所有参数作为源对象。然后把所有的源对象合并到目标对象中。它会修改了一个对象,因此触发ES6 setter。
  • 扩展运算发(…)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中。它不复制继承的属性或类的属性,但是它会复制ES6的symbols属性。

ES6

  1. 块级作用域:块级作用域由{ }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域可以解决ES5中的两个问题:
    1. 内层变量肯能覆盖外层变量
    2. 用来计数的循环变量泄漏为全局变量
  2. 变量提升:var存在变量提升,let和const不存在变量提升 ,即只能在变量声明之后使用否则会报错
  3. 给全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量可以为全局变量,并且会将改变量添加为全局对象的属性,但是let和const不会。
  4. 重复声明:var声明变量是,可以重复声明变量,后声明的同名变量会覆盖之前声明的。而const和let不允许重复声明变量。
  5. 暂时性死区:在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
  6. 初始值设置:在变量声明时,var,let可以不用设置初始值,但是const必须设置初始值。
  7. 指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

image.png

  1. 箭头函数比普通函数更加简洁
    1. 如果没有参数,直接协议可空括号即可
    2. 如果只有一个参数,可以省去参数的括号
    3. 如果由多个参数,用逗号分割
    4. 如果函数体返回值只有一句,可以省略大括号
    5. 如果函数不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常用的就是调用函数:
1
let fn = () => void doesNotReturn();
  1. 箭头函数没有自己的this

箭头函数不会创建自己的this,所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中this的指向在它定义时已经确定了,之后不会改变。

  1. 箭头函数继承来的this指向永远不会改变
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var id = 'GLOBAL';
var obj = {
id: 'OBJ',
a: function(){
console.log(this.id);
},
b: () => {
console.log(this.id);
}
};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'
new obj.a() // undefined
new obj.b() // Uncaught TypeError: obj.b is not a constructor

对象obj的方法b是使用箭头函数定义的,这个函数中的this就永远指向它定义时所处的全局执行环境中的this,即便这个函数是作为对象obj的方法调用,this依旧指向Window对象。需要注意,定义对象的大括号{}是无法形成一个单独的执行环境的,它依旧是处于全局执行环境中。

  1. call(),apply(),bind()等方法不能改变箭头函数中的this指向
1
2
3
4
5
6
7
8
9
var id = 'Global';
let fun1 = () => {
console.log(this.id)
};
fun1(); // 'Global'
fun1.call({id: 'Obj'}); // 'Global'
fun1.apply({id: 'Obj'}); // 'Global'
fun1.bind({id: 'Obj'})(); // 'Global'

  1. 箭头函数不能作为构造函数使用

构造函数在new的步骤在上面已经说过了,实际上第二步就是将函数中的this指向该对象。 但是由于箭头函数时没有自己的this的,且this指向外层的执行环境,且不能改变指向,所以不能当做构造函数使用。

  1. 箭头函数没有自己的arguments

箭头函数没有自己的arguments对象。在箭头函数中访问arguments实际上获得的是它外层函数的arguments值。

  1. 箭头函数没有prototype
  2. 箭头函数不能用作Generatir函数,不能使用yeild关键字

const保证的不是变量的值不能改变,而是变量指向的那个内存地址不能改动。对于基本类型的数据(数值,字符串,布尔值),其值就保存在变量指向的那个内存地址,因此等同于常量。
但是对于引用类型的数据(主要是对象和数组)来说,变量指向数据的内存地址,保存的只是一个指针,const只能保证这个指针式固定不变的,至于它指向的数据结构式不可变的,就完全不能控制了。

1
2
3
4
5
6
7
8
9
//a为引用类型数据时:
const a=["1","2","3"]
a.push("4")
console.log(a);//['1', '2', '3', '4']

//a为基本数据类型时
const a=1
a=2
console.log(a);//报错,error: Assignment to constant variable.

箭头函数是ES6中新提出来的,它没有自己的this指向,更不可以使用argyments参数,所以并不能够new一个箭头函数。
new操作符的实现步骤如下:

  • 创建一个对象
  • 将构造函数的租用与赋给新对象(也就是将对象的__proto__属性指向构造函数的prototype属性)
  • 指向构造函数中的代码,构造函数中的this指向对象(也就是为这个对象添加方法和属性)
  • 返回新的对象

因此,由于箭头函数不能执行第二步和第三步所以并不能new一个箭头函数。

随着ES6的发布 ,可以发现它除了新增了模板语法外(例如:aka${a}uio)外还新增了一系列的字符串方法用于提升开发效率。
(1)存在性判定:在过去判断一个字符/字符串是否在某字符串中时,只能用indexOf>-1来判断。现在ES6提供了三个方法:includes,startsWith,endWith,它们都会返回一个布尔值来告诉你是否存在。

  • includes:判断字符串与字符的包含关系:
1
2
3
const son = 'haha' 
const father = 'xixi haha hehe'
father.includes(son) // true
  • startsWith:判断字符串是否以某个/某串字符开头:
1
2
3
const father = 'xixi haha hehe'
father.startsWith('haha') // false
father.startsWith('xixi') // true
  • endsWith:判断字符串是否以某个/某串字符结尾:
1
2
const father = 'xixi haha hehe'
father.endsWith('hehe') // true

(2)自动重复:可以使用repeat方法来使同一个字符串输出多次(被连续复制)

1
2
3
4
const sourceCode = 'repeat for 3 times;'
const repeated = sourceCode.repeat(3)
console.log(repeated) // repeat for 3 times;repeat for 3 times;repeat for 3 times;