秋招复习计划-JavaScript知识点1

  |  

前言

这部分复习JavaScript知识点中的1-4点:原始值和引用值类型及其区别、如何判断数据类型、类数组与数组的区别和转换、数组常见API


23


原始值和引用值类型及其区别

原始值和引用值包括的类型

在JavaScript中,变量可以存放两种类型的值:原始值和引用值

原始值代表原始数据类型的值,也叫基本数据类型,包括:Number、String、Boolean、Null、Undefined

引用值指的是符合数据类型的值,包括Object(包括数组)、Function、Date、RegExp

原始值和引用值的存储方式

原始值是存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。

1
2
var x = 1;
// 1就是一个原始值,变量x中存放的就是原始值本身1

引用值是存储在堆中的对下个你,这个时候变量中存放的是对象的指针,指向对象的内存空间。(注意引用值是一个对象,而不是一个指针)

1
2
var o = {};
//这个对象就是一个引用值,变量o并不是存放这个对象,而是存放着这个对象的指针

变量赋值过程

为变量赋值时,ECMAScript的解释程序必须判断值是原始类型还是引用类型。要实现这一点,解释程序则需尝试判断该值是否为ECMAScript的原始类型之一。由于这些原始类型占据的空间是固定的,所以可以将它们存储在较小的内存区域-栈中。这样存储便于循序查找变量的值。

注意:在许多语言中,字符串被看作引用类型,而非原始类型,因为字符串的长度是可变的。但是ECMAScript仍将字符串视作原始类型。

如果一个值是引用类型,那么它的存储空间将从堆中分配。因为引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查询的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的长度固定的,所以地址存储所占的空间在栈中也是固定,所以把地址存储在栈中对变量性能没有任何负面影响

可以用一张图来清晰的表示原始类型和引用类型在堆栈中的存放方式

变量赋值

原始和引用值的传递问题

注意,这里是笔试和面试的重点,主要就是考察对于之前的内容是否理解。

当我们把一个原始变量传递给另一个原始变量时,是把一段栈空间的内容复制到另一段栈空间,两个原始值互不影响。

1
2
3
4
5
6
let a = 1;
let b = a;
console.log(b); // 1
b = 2;
console.log(b); // 2
console.log(a); // 1

而当把引用类型值的变量传递给另一个变量时,复制的其实是指向实际对象的指针,此时当我们通过方法,修改其中一个变量引用的对象的属性后,访问另一个变量是,其值也会随之改变。因为从本质来讲,两个变量存放的是同一个地址指针,引用的是同一个对象。

1
2
3
4
5
6
7
8
let a = {
value: 1
}
let b = a;
console.log(b.value); // 1
let b.value = 2;
console.log(b.value); // 2
console.log(a.value); // 2

如何判断数据类型

typeof

typeof是一个一元操作符,其右侧跟一个一元表达式,并返回这个表达式的数据类型。返回结果用该类型的字符串(全小写)形式表示,包括以下7中:number、boolean、symbol、string、object、undefined、function

1
2
3
4
5
6
7
8
9
10
typeof ''; // sting 有效判断
typeof 1; // number 有效判断
typeof Symbol(); // symbol 有效判断
typeof true; // boolean 有效判断
typeof undefined; // undefined 有效判断
typeof null; // object 无效判断
typeof []; // object 无效判断
typeof new Function(); // function 有效判断
typeof new Data(); // object 无效判断
typeof new RegExp(); // object 无效判断

typeof操作符会返回一些技术上正确但是判断不是很准确的值:

  • 对于基本类型,除null以外,均可以返回正确的结果
  • 对于null,返回object类型
  • 对于引用类型,除function以外,一律返回object类型

其中null有自己的数据类型Null,引用类型中的数组、日期、正则都有自己的具体类型,但是typeof都不能准确的判断,而是返回了处于这些类型原型链顶端的Object类型

instanceof

instanceof用来判断A是否是B的实例,及A instanceof B,如果是,则返回true,否则返回false。我们可以用代码来表示instanceof实现的逻辑

1
2
3
4
5
6
instanceof(A, B) = {
if(A.__proto__ === B.prototype) {
return true;
}
return false;
}

他其实就是判断A的__proto__属性所引用的原型对象是否为B的原型

来看几个示例

1
2
3
4
5
"1" instanceof String; //true
1 instanceof Number; //true
true instanceof Boolean; //true
new Date() instanceof Date; //true
new RegExp() instanceof RegExp; //true

当然存在一些特殊的情况

首先,并不存在

1
2
undefined instanceof Undefined;
null instanceof Null

其次,虽然对null使用typeof返回的是object,但是null instanceof Object返回的是false。严格来说null是JS设计的一个败笔,当设计者准备改null的类型为Null时,大量网站已经开始使用JS并且接受了null,如果更改就会出现大面积的问题,所以null的类型也就不了了之了。

1
2
[] instanceof Array; // true
[] instanceof Object; // true

数组是Array的实例也是Object的实例,这是因为原型链的关系

1
2
[].__proto__  ->  Array.prototype
Array.prototype.__proto__ -> Object.prototype

所以数组既是Array的实例也是Object的实例,以此类推,Date和RegExp以及其它对象也是同样的道理。因此instanceof只能判断两个对象是否属于实例关系,无法判断一个对象实例具体属于哪种类型。

但是instanceof存在一个缺陷,就是它假定只有一个全局执行环境。如果在网页中包含多个框架,那实际上就存在两个以上不同的全局执行环境,从而存在两个以上不同版本的构造函数。譬如,当我们从一个框架向另一个框架传入一个数组,那么传入的数组与在第二个框架中原生创建的数组分别具有各自不同的构造函数

1
2
3
4
5
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
xArray = window.frames[0].Array;
var arr = new xArray(1,2,3); // [1,2,3]
arr instanceof Array; // false

为了弥补数组的这个问题,ES5提供了专门针对数组的检测方法,Array.isArray()。改方法用以确定某个对象本身是否为Array类型,而不区分该对象实在哪个环境中创建的。

Array.isArray()本质上是检测对象的[[Class]]值,[[Class]]值是对象的一个内部属性,里面包含了对象的类型信息,其格式为[object Xxx],其中Xxx就是对应的具体类型,对于数组[[Class]]的值就是[object Array]

constructor

我们知道当一个函数被定义时,JS引擎就会为函数添加prototype原型,prototype原型有一个constructor属性,指向函数的引用。

constructor1

当执行var f = new F()时,F就被当作构造函数,f就成为了F的实例对象,此时f.constructor也就是f.__proto__.constructor为F。从原型链的角度来看,构造函数F就是新对象的类型。这样做的意义在于让新对象在诞生以后,就具有可追溯的数据类型

JavaScript的内置对象在内部构建时也是这样的

constructor2

但是通过构造函数的形式来判断数据类型也是有缺陷的:

  • null和undefined是无效的对象,因此是不会有constructor存在,这两种类型的数据需要通过其它方式来判断
  • 函数的constructor是不稳定的,这主要体现在函数的原型是能被重写的,当原型被重写时,原有的constructor引用会丢失,constructor会默认为Object

constructor3

prototype被重写为一个新对象,新对象是new Object()的字面量,因此new Object()会将Object原型上的constructor传递给新对象。

Object.prototype.toString.call()

toString()是Object的原型方法,调用该方法,默认返回当前对象的[[Class]]。[[Class]]值是对象的一个内部属性,里面包含了对象的类型信息,其格式为[object Xxx],其中Xxx就是对应的具体类型。

对于Object对象,直接调用toString()就能返回[object Object]。而对于其它对象,则需要通过call或apply来绑定调用的对象,就能返回对象正确的类型信息

1
2
3
4
5
6
7
8
9
10
11
12
13
Object.prototype.toString.call('') ;   // [object String]
Object.prototype.toString.call(1) ; // [object Number]
Object.prototype.toString.call(true) ; // [object Boolean]
Object.prototype.toString.call(Symbol()); //[object Symbol]
Object.prototype.toString.call(undefined) ; // [object Undefined]
Object.prototype.toString.call(null) ; // [object Null]
Object.prototype.toString.call(new Function()) ; // [object Function]
Object.prototype.toString.call(new Date()) ; // [object Date]
Object.prototype.toString.call([]) ; // [object Array]
Object.prototype.toString.call(new RegExp()) ; // [object RegExp]
Object.prototype.toString.call(new Error()) ; // [object Error]
Object.prototype.toString.call(document) ; // [object HTMLDocument]
Object.prototype.toString.call(window) ; //[object global] window 是全局对象 global 的引用

其实这个方法在ES6版本出现之前是没有缺陷的,这也是jQuery框架使用这种方法进行数据类型检测的原因。

ES6提出了符号的概念,其中有一个知名符号Symbol.toStringTag,该符号是所有对象都具有的一个属性,它的功能就是定义了当对象被Object.prototype.toString.call()调用时返回的值,这也就是为什么这种方式也存在缺陷了

toString

虽然这样的行为并不被推荐,但是语言并没有禁止这样的行为,所以现在这种方法也不能确保百分之百的正确了

类数组与数组的区别和转换

类数组的定义

  • 拥有length属性,其它属性的索引为非负整数
  • 并不具有数组所具有的方法

类数组转换为数组

方法一:Array.prototype.slice.call(arrLike)

这样就能将一个类数组对象转换成一个数组对象了。

想要理解为什么这个方法能将类数组对象转换为数组对象,首先我们得了解slice这个方法

slice是存在于数组类型Array原型上的默认函数,他的使用方法如下

arrayObject.slice(start, end)

其中start表示开始下标,end表示结束下标。该方法返回一个新的数组,包含从start到end的arrayObject中的元素。且返回的是一个新的数组,是原数组部分的深拷贝。

我们可以用JS代码来模拟slice的实现原理

1
2
3
4
5
6
7
8
9
Array.prototype.slice = function(start, end) {
var result = [];
start = start || 0;
end = end || this.length;
for(var i = 0; i < end; i++) {
result.push(this[i]);
}
return result;
}

由类数组的定义来看,当我们将类数组对象作为slice执行的this时,是可以将类数组对象中的属性值放到result数组中并返回的,所以我们可以用这样的方法来将类数组对象转换为数组。此外还有类似的方法

[].slice.call(arrLike)

通过这个方法,我们可以定义一个将类数组对象转化为数组的通用函数

1
2
3
4
5
6
7
8
9
10
11
var toArray = function(s) {
try {
retrun Array.prototype.slice.call(s);
} catch(e) {
var arr = [];
for(var i = 0, len = s.length; i < len; i++) {
arr[i] = s[i];
}
return arr;
}
}

此外结合之前的原始值和引用值的知识点,我在考虑在类数组对象转换为数组对象的过程中push这个操作所谓的深拷贝在类数组对象的属性值为对象的情况下,将其转换为数组后,对数组值的操作是否会影响到原有类数组对象的值,按照引用值传递的过程来说,是会有所影响的

arrLike1

方法二:Array.from(arrLike)

Array.from是ES6为数组提供的新的方法,其作用就是将参数转换为数组

1
2
3
4
Array.from('一二三四五');
Array.from(new Set([1, 2, 3, 4]));
Array.from(new Map([[0, 10], [1, 20], [2, 30]]));
Array.from(arguments);

方法三:对于可迭代的类数组对象可以使用扩展运算符

let arr = [...iteratorableArrLike]

区别

类数组对象和数组最主要的区别就是在于进行类型判断的时候

arrLike2

数组常见API

Array对象提供常用的方法包括:toString,join,push,pop,unshift,shift,concat,slice,reverse,sort,toLocaleString

toString

toString方法将数组表示为字符串,各个元素按顺序排列组合成字符串返回,语法如下

array.toString()

join

join方法也是将各个元素组合成字符串,但连接符号可以自己指定,语法如下

array.join("连接符号")

push

push方法用作一次向数组末尾添加单个或多个元素,也可以添加数组,语法如下

array.push("元素1", "元素2",....)

unshift

unshift方法将元素添加到数组首部,一次可以插入单个或多个元素,所有元素按顺序插入,操作完成后返回新数组的引用,语法如下:

array.unshift("元素1","元素2",.....)

pop

pop的作用是移除数组末尾的一个元素。删除数组中的元素还可以使用delete 数组名[下标],不过与delete不同的是,pop方法删除最后一个元素后,还将返回其引用,语法如下

array.pop()

shift

shift与pop相反,移除数组的第一个元素并将其返回,该方法执行后,数组剩下的元素向前移,下标索引号重新调整从0开始,语法如下:

array.shift()

concat

concat方法可以将多个数组元素连接在一起成为新的数组,语法如下:

array.concat(item1, item2, ...)

splice

splice方法的作用是从一个数组中移除一个或多个元素。剩下的元素组成一个数组,移除的元素组成另一个数组并返回它的引用。同时,原数组可以在移除的开始位置处顺带插入一个或多个新元素,达到修改替换数组元素的目的,语法如下

array.splice(start, deleteCount, item1, item2, ...)

  • start:必选项,表示从数组中剪切的其实位置下标索引号
  • deleteCount:必选项,表示从数组中切取的元素个数
  • item:可选项,表示切取时插入原数组切入点开始处的一个或多个元素

slice

slice方法的作用是抽取数组的一段元素,抽取指定下标索引区间中的元素元素作为新数组返回,语法如下:

array.slice(start, end)

文章目录
  1. 1. 原始值和引用值类型及其区别
  2. 2. 如何判断数据类型
  3. 3. 类数组与数组的区别和转换
  4. 4. 数组常见API
|