JavaScript基础数据结构

182 阅读13分钟

数据分类、判断、转换

数据分类
因为JavaScript原型链的机制,当用到一些可能被对象重写的通用方法,可以借助内置类的方法和call来完成

引用类型(值存储在堆):Object、Array
值类型(值存储在栈):

非空类型:Boolean、Number、String、Symbol
空类型【底型】:Undefined、Null 最空的对象

对于基本类型来说,如果使用字面量的方式,那么这个变量只是个字面量,只有在必要的时候才会转换为对应的类型

typeof操作符检测原始值很有效

  1. 不能区别未初始化变量和未定义变量
  2. 不能区别null、object、array
  3. typeof NaN是number

variable instanceof constructor 操作符返回布尔
关于使用instanceof的前提:只有一个全局执行上下文。如果网页里有多个框架,则可能涉及多个版本的内置类型的构造函数。如果数组在a上下文创建,在b使用instanceof判断,就会false。

最好使用专用的判断方法,e.g: Array.isArray、isNaN

类型转换

对象在转换基本类型时,顺序查找 Symbol.toPrimitive、valueOf 、toString方法,找到一个就调用返回。

let a = {
  [Symbol.toPrimitive]() {
    return 2;
  },
  valueOf() {
    return 0;
  },
  toString() {
    return '1';
  },
}
1 + a // => 3
'1' + a // => '12'

加法运算:其中一方是字符串类型,就会把另一个也转为字符串类型。
其他运算只要其中一方是数字,那么另一方就转为数字。

'a' + + 'b' // -> "aNaN"
[] == ![] // -> true

对象

浅拷贝&深拷贝

浅拷贝(相互影响):把一个引用A的堆地址给了另一个引用A1
深拷贝:新开辟了一个堆地址内存,把原对象中的所有属性、方法一个一个找到存储进去

注意:属性也是一个引用的时候,即需要留意对象的层级,属性是否是一个引用、以及对应引用的属性是否又是一个引用。存储在对象容器内部的是对象属性的名称,名称就像指针一样,指向指真正的存储位置

浅拷贝: 符号 = ,splice、fill(value浅拷贝, start, end)、push、 pop、 shift、unshift、
深拷贝: concat 、 slice、 js扩展运算符(...)、flat、 Array.from,这部分只是一级引用的深拷贝

对象的扩展运算符(…)用于取出参数对象的所有可枚举属性,拷贝到当前对象之中

JSON.parse(JSON.stringify(old))是彻底深拷贝 JSON.parse(JSON.stringify(old))弊端

  1. 不管这个对象原来的构造函数是什么,在深拷贝之后都会变成Object
  2. 无法正确处理循环引用
  3. 对于正则表达式类型、函数类型、 undefined 或者 symbol等无法进行深拷贝(而且会直接丢失相应的值)
function deepClone(obj){
           //判断obj是否是数组
        let objClone = Array.isArray(obj)?[]:{};  
        if(obj && typeof obj==="object"){
            for(key in obj){
                if(obj.hasOwnProperty(key)){
                    //判断ojb子元素是否为对象,如果是,递归复制
                    if(obj[key]&&typeof obj[key] ==="object"){
                        objClone[key] = deepClone(obj[key]);
                    }else{
                        //如果不是,简单复制
                        objClone[key] = obj[key];
                    }
                }
            }
        }
        return objClone;
 } 

Object.assign 浅

Object.assign
将所有 可枚举和自有属性、键为符号的属性 从一个或多个源对象分配到目标对象,返回目标对象。源和目标对象共享一个引用数据

可枚举属性

前文提到:扩展符、Object.assign也是只能获取到对象的可枚举属性

可枚举属性: 属性的描述符号 “可枚举” 标志enumerable值为 true
通过直接的赋值和属性初始化的属性默认为true;
通过 Object.defineProperty 等定义的属性默认为 false

判断属性的性质

p.s. 判断属性是否存在

hasOwnProperty方法是通过判断该属性是否直接属于某个对象决定的,而不是通过原型链继承的。
即使属性的值是 null 或 undefined,只要属性存在,hasOwnProperty 依旧会返回 true。

注意: in 运算符不同,是另一角度,表示可访问,包括从原型链上继承到的属性、不可枚举属性,但是会排除 Symbol属性

所有对象都可以通过object.prototype访问到hasOwnProperty方法,对于没有链接object.prototype的对象,可以object.prototype.hasOwnProperty.call(obj, 'keyName') 判断是否可枚举属性
hasOwnProperty 获取属性后使用 propertyIsEnumerable 过滤可枚举属性

var buz = {
  fog: 'stack'
};

for (var name in buz) {
  if (buz.hasOwnProperty(name)) {
    console.log('this is fog (' +
      name + ') for sure. Value: ' + buz[name]);
  }
  else {
    console.log(name); // toString or something else
  }
}

const object1 = {};
const array1 = [];
object1.property1 = 42;
array1[0] = 42;

console.log(object1.propertyIsEnumerable('property1'));
// expected output: true

console.log(array1.propertyIsEnumerable(0));
// expected output: true

console.log(array1.propertyIsEnumerable('length'));
// expected output: false

console.log(array1.hasOwnProperty('length'));
// expected output: true

注意:JavaScript 并没有保护 hasOwnProperty 这个属性名 对象可能自有一个占用该属性名的属性时,就需要使用外部的 hasOwnProperty 获得正确的结果:

var foo = {
  hasOwnProperty: function() {
    return false;
  },
  bar: 'Here be dragons'
};

foo.hasOwnProperty('bar'); // 始终返回 false

// 如果担心这种情况,
// 可以直接使用原型链上真正的 hasOwnProperty 方法
({}).hasOwnProperty.call(foo, 'bar'); // true

// 也可以使用 Object 原型上的 hasOwnProperty 属性
Object.prototype.hasOwnProperty.call(foo, 'bar'); // true

创建属性Object.defineProperty()

Object.defineProperty(obj, 'key', descriptor可选);
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。 注:应当直接在 Object 构造器对象上调用此方法,而不是在任意一个 Object 类型的实例上调用:

ES5新增对象描述符

var o = {
 get b(){
  return 2
 }
}; // 创建一个新对象

// 在对象中添加一个属性与数据描述符的示例
Object.defineProperty(o, "a", {
  value : 37,
  writable : true,
  enumerable : true,
  configurable : true
  get: function(){return this.a}
});

注意get、set通常是成对出现

Object.keys

(对象),返回一个对象所含属性的数组。由此得以得出对象的属性的个数等

只对非法标识符的属性使用引号"key-name"

对象的不可变性

  • writable:false和configurable:false不可修改、重定义、删除
  • Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性并且保留已有属性
  • Object.seal()方法封闭一个对象,Object.preventExtensions()+所有属性configurable:false
  • Object.freeze() 方法可以冻结一个对象【自己,不包括原型链】, Object.seal()+所有属性writable:false。注意冻结一个对象后该对象的原型也不能被修改。 三个方法均返回和传入的参数相同的对象

访问对象属性:属性访问、键访问

注意:访问object的键有点语法、中括号语法,在定义object的属性时可以采用数值、字符串、符号为键,最终都会转换为string,中括号语法会在转换前先执行计算

一般来说开发不会用到符号实际值(理论上不同JavaScript引擎值不同),通常接触到符号的名称 map可以采用JavaScript所有类型为键,map内部采用sameValueZero比较(类似===)来匹配键

.a要求属性名满足标识符的命名规范 [a]任意UTF-8/Unicode字符串、可计算表达式作为属性名、Symbol

var s=Symbol("hh");
let user = { // 属于另一个代码
 id: "John",
 [s]:"dd"
};
console.log(typeof(( user[s] ))); // string
console.log(typeof(s)); // Symbol
console.log(Symbol.hh); // undefined
user[s]="ss";
console.log(( user.s )); // undefined
console.log(( user[s] )); // ss
console.log(( user.id )); // John 

去重

原理:对象的属性key不能重复+方括号可计算

function noRepeat(arr){
  var i=-1,
    obj={},
    res=[];
  while(++i<arr.length){
    obj[arr[i]]||res.push(arr[i]);
    obj[arr[i]]=true;
  }
  return res;
}

console.log(noRepeat([2,2,4,3,4,3]));
 

Array

flat 深

flat( depth可选 ) 方法会按照depth指定深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组,会移除数组中的空项,返回

嵌套数组调用flat不会影响内嵌数组

depth 默认为1、也支持 Infinity关键字作为参数

flatMap()方法对原数组的每个成员执行传入的函数参数,即Array.prototype.map(), 然后对返回值组成的数组执行flat()方法。


let arr1 = ["it's Sunny in", "", "California"];

arr1.map(x => x.split(" "));
// [["it's","Sunny","in"],[""],["California"]]

arr1.flatMap(x => x.split(" "));
// ["it's","Sunny","in", "", "California"]



arr1.map(x => [x * 2]);
// [[2], [4], [6], [8]]

// only one level is flattened
arr1.flatMap(x => [[x * 2]]);
// [[2], [4], [6], [8]]

自实现

// 一级展开:

arr.reduce((acc, val) => acc.concat(val), []);
// [1, 2, 3, 4]

// 使用扩展运算符 ...
const flattened = arr => [].concat(...arr);

// 深拉平

var arr1 = [1,2,3,[1,2,3,4, [2,3,4]]];

function flatDeep(arr, d = 1) {
   return d > 0 ? arr.reduce((acc, val) => acc.concat(Array.isArray(val) ? flatDeep(val, d - 1) : val), [])
                : arr.slice();
};

flatDeep(arr1, Infinity);
// [1, 2, 3, 1, 2, 3, 4, 2, 3, 4]

归并reduce

reduce() 方法接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。可以作为一个高阶函数。
该函数内部前一个参数是一个箭头函数,(此时箭头函数的参数即reduce的默认参数列表如下),后一个是函数起始值(不赋值,少一次迭代),作为第一次reduce的total,箭头函数的返回值作为下一次reduce的total

[1,2,3].reduce(function(accumulator, currentValue, currentIndex, array){
	console.log(accumulator, currentValue, currentIndex, array);
        return `return${currentIndex}`;
}, 'firstValue');

firstValue 1 0 [1, 2, 3]
return0 2 1  [1, 2, 3]
return1 3 2  [1, 2, 3]
 const map = function map (arr, fn) {
     return arr.reduce(
        (lastReturn,ele)=>{
            return [...lastReturn,fn(ele)]
        },
        []
    )
    }
const arr = [2,4,5,6]
console.log(map(arr, n => n * 2))
    
const mforEach = function (arr, fn) {
      return arr.reduce(
       (lastReturn, ele) => {
         fn(ele) 
        },
          []
     )
    }
    const arr = [2,4,5,6]
    console.log(mforEach(arr,n => console.log(n)))
    console.log(arr.forEach(n => console.log(n)))

数组解构

表达式是惰性求值,只有在用到才会求值
解构中,在等式左边使用表达式给数组元素赋默认值,只有当右边没有该元素的值的时候才会执行表达式。

let[a,b=3]=[1]; // b=3
let[a,b=fun()]=[1,2];  // b=2,fun不执行
let arr=['hhh','uuu','jjj'];
let [a,b,c]=arr;
console.log(a,b,c);//hhh
let[x,...y]=arr;//y==['uuu','jjj']
let [s,[d,f]]=[1,[2,3],4];//d==2,f==3
let [first,...end]="hello";//end==[e,l,l,o]
let m=new Map().set("one",1).set("two",1).set("o",1);//迭代Map返回数组
for(let [k,v] of m){console.log(k,v);}

伪数组

let fakeArray = {
    0:'a',
    1:'b',
    2:'c',
    length:3,
}
console.log(fakeArray[2]);
[].push.call(fakeArray,"d");// 更新length
console.log(fakeArray);
fakeArray[4]='e';// 不会更新length
console.log(fakeArray);

String

字符串是原始值,不涉及引用,所以对string进行修改的函数都是返回一个新的字符串,不会影响源字符串。

array、string共用的方法,数组使用就要注意引用问题,可能会改变原数组

indexOf(searchValue ,fromIndex) 
方法返回在数组中可以找到一个给定元素的第一个索引,如果不存在,则返回-1
方法返回调用它的 String 对象中第一次出现的指定值的索引,从 fromIndex 处进行搜索。如果未找到该值,则返回 -1

涉及切分: split, substring, substr(字符串)、slice、concat深(字符串/数组)、splice浅(数组)

// 切分
let s="hello world"
//单参
console.log(s.slice(3));//lo world
console.log(s.substring(3));
console.log(s.substr(3));
//两
console.log(s.slice(3,7));//lo w  3-6
console.log(s.substring(3,7));
console.log(s.substr(3,7));lo worl
//负参处理
console.log(s.slice(-3));//11-3=8 rld
console.log(s.substr(-3));//rld
console.log(s.substring(-3));//所有负参=0 hello world

// 定位
let mess="abcd";
console.log(mess.charAt(2); // c
console.log(mess.charCodeAt(2)); // 99
console.log(String.fromCharCode(0x63,0x64)); // c,d

// 包含
第二个参数缺省时,只在开头/结尾/全局匹配一次;
有第二个参数,就从指定位开始多次匹配
startsWith("字符串“,指定匹配起始位);
endsWith("字符串“,指定匹配起始位);
includes("字符串“,指定匹配起始位);

// 其他
trim();返回一个除掉开头和结尾空格的string副本
repeat(次数);返回重复拼接字符串
padStart(副本长度,“补足位符号串”);//默认补空格 实现右对齐
padEnd();

trim

trim的替代:正则
^\s+匹配开头的多个空白
\s+$ 匹配结尾的多个空白

function trim(str){
    if(str && typeof str === 'string'){
        return str.replace(/^\s+|\s+$/g,'');
    }
}

Null类型

不建议显式赋值undefined,在定义将来要保存对象值的变量时建议用null初始化

null标识尚未存在的对象,常用来表示函数企图返回一个不存在的对象。

{ }是一个不完全空对象,原型链上有Object,null为原型链顶端,是完全空对象,原型链也没有。

选择渲染DOM,DOM对象:null 与bind搭配

bind

bind返回一个原函数的拷贝,并拥有指定的 this 值和初始参数。不改变原函数的代码,来进行复用、扩展
如果 bind 函数的参数列表为空,或者thisArg是null或undefined,执行作用域的 this 将被视为新函数的 thisArg。

/**
 使一个函数拥有预设的初始参数
将初始参数作为bind()的参数写在this后面
 */
function addArguments(arg1, arg2) {
    return arg1 + arg2
}
 
var addThirtySeven = addArguments.bind(null, 37);
var result = addThirtySeven(5);
console.log(result);
result = addThirtySeven(5, 10);  // 注意这个,其实输出42,第二个10被丢弃了
console.log(result);

Boolean类型

Boolean(其他类型的变量);//返回布尔
空字符串、0、undefined、null、NaN这类基础类型转为false
其他的 [] {}等衍生类型都会被转为true

!!bool === bool;//只有布尔类型才会true
console.log(([]==false?true:false)); // true
console.log(([]==![]?true:false)); // true
console.log((false == null)?true:false) // false
console.log(({}==false)?true:false) // false

console.log((null===false)?true:false) // false

相等运算符

严格相等运算符:

  • 不同类型返回false
  • 对象——仅在指同一对象时返回true 相等运算符(==和!=):
  • 比较目标类型相同的时候,和严格相等一样
  • 比较目标类型不相同的时候:类型转换

Object.is

比较粒度上等同===,修复了一些情况

console.log([]===[]);//false
console.log(Object.is([],[]));//false
console.log({}==={});//false
console.log(Object.is({},{}));//false
console.log(-0===0&&0===+0);;//true
console.log(Object.is(+0,0));//true
console.log(Object.is(+0,-0));//false
console.log(Object.is(-0,0));//false
console.log(NaN===NaN);//false
console.log(isNaN(NaN));//true
console.log(Object.is(NaN,NaN));//true

Number类型

JS 不像其他语言,它只有一种数字类型Number,是浮点类型的,没有整型。 只有一种数值可以避免转换带来的问题,基于应用最简化设计,有使用需要时候引用必要的高精度整数库是一种很好的语言设计思想。

浮点类型

Object.is(0,-0); //false

字面量IEEE 754标准

JavaScript内置了很多不可变的number对象,每个对象代表一个数,数值字面量本质上是一个最接近的number对象的引用,有时完全重合有时会有误差。

number对象集就是基于IEEE 754标准实现的64位浮点数,在使用中会遇到某些 Bug: IEEE 754标准有两个零+-

位运算

&、|、^ 、<<、>>>、>>带符号右移、~

c语言的符号位扩展取决于数据类型、java和JavaScript的符号位扩展取决于运算符。

JavaScript在做位运算时候,会事先将其转换为32位有符号整型,得到结果再转换回JavaScript的数值类型。 虽然在54位安全整数类型上运算更优雅,但是JavaScript直接丢失高22位的有效数字,没有任何警告信息

常量

Number对象上还挂载一些特殊意义的常量

js数值类型的边界线
> Number.MIN_VALUE
< 5e-324
> Number.MAX_VALUE
< 1.7976931348623157e+308

IEEE 754这个规定中能安全的表示数字的范围在`-(253 - 1)` 到 `253 - 1之间`
Number.isSafeInteger(Number.MAX_VALUE);// false
> Number.MIN_SAFE_INTEGER
< -9007199254740991
> Number.MAX_SAFE_INTEGER
< 9007199254740991

> Number.MAX_VALUE===Number.MAX_SAFE_INTEGER*2**971
< true
 
 比最小正数小的正数+1 === 1
> Number.EPSILON
< 2.220446049250313e-16

转换、NaN

在比较大小之前一定要保证先转换为number,var 、let是弱的类型,默认是string,而js中字符串的比较是从左往右的,5比10大,因为编译器最先判断的是首位字符,5比1大

转换失败,程序不会抛出异常而是返回NaN

  • typeof NaN是’number‘
  • isNaN();是否不能转化为数字
  • 八进制0开头
  • 十六进制0x开头
  • js保存数据的范围在 Number.MIN_VALUE——Number.MAX_VALUE,超过这个范围就是+Infinity或者-Infinity
  • 用isFinite(数值)返回布尔值,是否有限
NaN===NaN;//false
isNaN("10");//false
isNaN(“blue");//true
Number.isNaN(true);//false可以转换为1

Number()
0: null、""、[]
NaN: undefined、非进制数表达字符串、{}

Number <=> 字符串

let num=10;
console.log(num.toFixed(2));//10.00
parseInt();//从第一个非空字符串开始,顺序检测+—数值、至非数值字符,没有数值字符=>NaN;可以指定底数(进制)
parseFloat();

取整

//取整数部分 
console.log(parseInt(5.57));//5
console.log(parseInt(5.3));//5
console.log(parseInt(-5.57));//-5
console.log(parseInt(-5.3));//-5
//整数四舍五入,再拼接符号
console.log(Math.round(5.57));//6
console.log(Math.round(5.3));//5
console.log(Math.round(-5.57));//-6
console.log(Math.round(-5.3));//-5
//大于等于自己的第一个整数
console.log(Math.ceil(5.57));//6
console.log(Math.ceil(5.3));//6
console.log(Math.ceil(-5.57));//-5
console.log(Math.ceil(-5.3));//-5
console.log(Math.ceil(5));//5
//小于等于自己的第一个整数
console.log(Math.floor(5.57));//5
console.log(Math.floor(5.3));//5
console.log(Math.floor(-5.57));//-6
console.log(Math.floor(-5.3));//-6
console.log(Math.floor(-5));//-5

Number.parseInt和Math.trunc 数据格式

parseFloat

// 返回3.14
parseFloat(3.14);
parseFloat('3.14');
parseFloat('  3.14  ');
parseFloat('314e-2');
parseFloat('0.0314E+2');
parseFloat('3.14some non-digit characters');
parseFloat({ toString: function() { return "3.14" } });

// 返回NaN
parseFloat("FF2"); 


+:拼接or加法

运算符右边跟一个操作数 +'2’是执行的第一个运算,+
两个操作值中只要有一个是string,执行的就是拼接操作,但是单独一个 + string会将其转换为数字

console.log(+'sad'); //  NaN
console.log(typeof(+'sad')); //  number

console.log(+'2');// 2
console.log(1+ +'2'+'2');//32
console.log(+'2'+2); //4
console.log(1+'2');//12
console.log('2'+1); //21

console.log(typeof(+'2')); // number
console.log(typeof(1+  )); // number
console.log(typeof(1+ +'2')); // number
console.log(typeof(1+ +'2'+'2'));// string
console.log(typeof(+'2'+2)); // number
console.log(typeof(1+'2')); // string
console.log(typeof('2'+1)); // string

Symbol

  • 符号是原始值
  • 符号实例是唯一的、不可变的,符号用作属性没有字面量语法
  • 创建symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性(无论是符号属性还是字符串属性)
  • Symbol()不能与new关键字一起作为构造函数来使用(避免创建符号包装对象)
  • 根据规范,对象的属性键只能是 String 类型或者 Symbol 类型。

Symbol为键的弊端

  • for…in 迭代、Object.getOwnPropertyNames() 不会返回 symbol 对象的属性,但是in、hasOwnProperty、 Object.getOwnPropertySymbols() 可以。
  • 当使用 JSON.stringify() 时,以 symbol 值作为键的属性会被完全忽略

符号注册表中有则返回,如果没有就创建

let globalSym = Symbol.for("foo");
console.log(Symbol.keyFor(globalSym)); // "foo"
console.log(Symbol.for("foo")); // Symbol("foo")

Map

相较object:

  • object只能采用数值、字符串、符号为键,而map可以采用JavaScript所有类型为键,map内部采用sameValueZero比较(类似===)来匹配键
  • 相同数据量,map占的存储单元更小
  • map删除更安全,object delete饱受诟病
let m=new Map().set("one",1).set("two",1).set("o",1);
// 等同于用二维数组初始化
let m=new Map([["one",1],["two",1],["o",1]]);

set(key,value);
get(key);//返回value
has(key);//返回布尔值
delete(key); // 删除对象中的属性
clear();
entries();
keys();
values();

object delete饱受诟病的原因

  • 如果试图删除的属性不存在,那么delete不会作用,但仍会返回true
  • delete操作只会在自身的属性上起作用,如果对象的原型链上有同名的属性,那么删除属性之后,对象会使用原型链上的那个属性
  • 任何使用 var 声明的属性不能从全局作用域或函数的作用域中删除。 这就导致
  1. delete操作不能删除任何在全局作用域中的函数(无论这个函数是来自于函数声明或函数表达式)
  2. 除了在全局作用域中的函数不能被删除,在对象(object)中的函数是能够用delete操作删除的。
  • 任何用let或const声明的属性不能够从它被声明的作用域中删除。
  • 不可设置的(Non-configurable)属性不能被移除。这意味着像Math, Array, Object内置对象的属性以及使用Object.defineProperty()方法设置为不可设置的属性不能被删除。