之前归纳了一下基本数据类型以及基本数据类型(也称为原始类型)的一些特性。在js当中,除了基本数据类型之外就是对象了,对象是属性的集合,每一属性都由键/值对构成,也可将对象看成是字符串到值的映射。
全局对象/包装对象
全局对象的属性是全局定义的符号,js程序可以直接使用,在js解释器启动的时候,它将创建一个新的全局对象,并定义一组初始属性,包括全局属性,全局函数,构造函数和全局对象(Math,JSON等)
包装对象是用来存取字符串,数字或布尔值属性时创建的临时对象,因为基本数据类型本身是不会有别的属性的,但是它们又有方法,比如之前说的str.trim()
,这个’.trim()'其实是对象形式引用的属性值。在处理这种属性引用的时候会先创建这个临时对象,在引用完毕之后就会自动销毁。
对象与基本类型的转换
Js对象有两个不同的方法(toString和valueOf)执行对象和基本类型的转换,不同的类定义了其特定的toString方法,valueOf方法则一般默认返回对象本身。
对象->布尔值:全是true
对象->字符串
- 如果对象有toString方法,调用这个方法,如果返回原始值,将原始值转为字符串并返回
- 如果对象没有toString方法,或者没有返回一个原始值,js会调用valueOf()方法,如果返回原始值,将原始值转为字符串并返回
对象->数字
- 如果对象有valueOf方法,且返回一个原始值,js将原始值转换为数字并返回
- 如果没有valueOf方法,但有toString方法,且返回一个原始值,js将其转换之后的字符串再转换为数字并返回
对象简述
对象的分类和属性的分类如下:
- 内置对象:由ECMAScript规范定义的对象或类,例如:数组,函数,日期,正则表达式,Map,Set等等
- 宿主对象:由js解释器所嵌入的宿主环境定义的,比如HTMLElement对象。因为宿主环境定义的方法可以当成普通的js函数对象,所以宿主对象也可以当作内置对象
- 自定义对象:用户自定义的对象
- 自有属性:直接在对象中定义的属性
- 继承属性:在对象的原型对象中定义的属性
对象最常见的用法是创建,设置,查找,删除,检测和枚举它的属性,对象的属性还拥有可写,可枚举,可配置三种特性,可以对这三种特性进行配置。
- 可写:是否能够修改属性值,默认是true
- 可枚举:能否通过for-in循环,或者是否能用Object.keys返回属性,直接在对象上定义的属性一般为true
- 可配置:是否能通过delete删除属性从而重新定义属性,是否修改属性的特性,是否能把属性修改为访问器属性
实际上ECMAScript有两种属性:数据属性和访问器属性,数据属性包括值,可写,可枚举,以及可配置;访问器属性的可写由setter控制,因此它包括get,set,可枚举和可配置。es5定义了一个名为“属性描述符”的对象来代表这四个特性,并通过object.getOwnPropertyDescriptor()
来获得特定属性的属性描述符(只有自有属性),要想获取对象原型的某个属性的属性描述符,需要使用object.getPrototypeOf()
获取对象原型,之后再进行遍历。
此外,每个对象还包含三个相关的对象特性,分别为:
- 对象的原型:用来继承属性
- 对象的类:一个字符串,用以表示对象的类型信息
- 对象的可扩展性:用以表示是否可以给对象添加新属性,可以用
object.isExtensible
判断对象是否可扩展,将对象设为不可扩展可以通过object.seal(),object.preventExtension(),object.freeze()
设定。- object.preventExtension(o):将o设置为不可扩展,不能添加新的属性,但是可以删,也不会影响原型链
- object.seal(o):将o设置为不可扩展,同时将它所有的自有属性设置为不可配置(不能加也不能删),但是不会影响原型链
- object.freeze(o):除了不能增删之外还不能修改,冻结后对象的原型也不能被修改。如果一个属性的值是对象,那这个对象中的属性还是可以修改的
对象属性配置
需要改变属性默认的特征,必须使用特定的方法,目前常用的方法如下:
object.defineProperty()
,object.defineProperties()
,new Proxy()
在说这三种方法之前,需要先说明一下访问器属性的两个函数:getter和setter,顾名思义,这俩就是用来存和取值的。使用它也很简单,将访问器定义为一个或两个和属性同名的函数,函数的定义用get和set关键字代替function来声明函数:
// 这段代码简单定义了一个拥有访问器属性的对象
var o = {
data_prop:value,
get accessor_prop(){/* main */}
set accessor_prop(value){/* main */} //将value进行定义
}
这三个方法可以通过getter和setter将对象的属性进行修改,三种方法在使用上又有所不同。
-
object.defineProperty()
Object.defineProperty(order, "tabLabel", { get: function () { //取值的时候会触发 this.extraOperateDom.innerHTML = "hehheh"; console.log("order: ", order.tabLabel); return order.tabLabel; }, set: function (value) { //更新值的时候会触发 let data = value; this.extraOperateDom.innerHTML = "hehheh"; console.log("set: ", data); }, });
Object.defineProperty(obj, prop, descriptor)方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回该对象。
-
object.defineProperties()
Object.defineProperty(order, { tabLabel:{ get: function () { //取值的时候会触发 this.extraOperateDom.innerHTML = "hehheh"; console.log("order: ", order.tabLabel); return order.tabLabel; }, set: function (value) { //更新值的时候会触发 let data = value; this.extraOperateDom.innerHTML = "hehheh"; console.log("set: ", data); } }, tabValue:{ get: function () { //取值的时候会触发 console.log("order: ", order.tabValue); return order.tabLabel; }, set: function (value) { //更新值的时候会触发 let data = value; console.log("set: ", data); } }, });
object.defineProperties()和上面那个函数是差不多的,只不过它可以一次性定义多个属性,而楼上一次只能定义一个属性
-
proxy()(vue3使用它来替代了object.defineProperty)
let obj={a:1,b:{c:2}}; let handler={ get:function(obj,prop){ const v = Reflect.get(obj,prop); if(v !== null && typeof v === 'object'){ return new Proxy(v,handler);//代理内层,Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。这些方法与proxy handlers的方法相同 }else{ return v; // 返回obj[prop] } }, set(obj,prop,value){ return Reflect.set(obj,prop,value);//设置成功返回true } }; let p=new Proxy(obj,handler);
proxy用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等),使用的时候需要先声明对象,target 是要使用Proxy包装的目标对象(啥对象都可以,只要是对象),handler是对象对应的一些操作,这样就可以在handler里面对对象进行更多更加有趣的操作。
Proxy代理对象时只会在调用时递归,不会一开始就全部递归,优化了性能 -
对比
- Proxy比Object.defineProperty使用方便。
- Proxy代理整个对象,啥对象都可以,Object.defineProperty只是代理对象上的某个属性。
- 如果对象内部要全部递归代理,则Proxy可以只在调用时递归,而Object.defineProperty需要在一开始就全部递归,Proxy性能优于Object.defineProperty。
- 对象上定义新属性时,Proxy可以监听到,Object.defineProperty监听不到。
- 数组新增删除修改时,Proxy可以监听到,Object.defineProperty监听不到。
- Proxy不兼容IE,Object.defineProperty不兼容IE8及以下。
对象的增删查以及数据检测
创建新对象
-
对象字面量:直接通过声明对象创建新对象
-
通过new创建(构造函数模式)
function Person(age,name,job){ this.name = name; this.age = age; this.job = job; } var person1 = new Person(18,'lalala','worker')
这样子调用构造函数会经历以下四个步骤:
- 创建一个对象
- 将构造函数中的作用域赋给新对象(this指向了这个新对象)
- 执行构造函数中的代码(为新对象添加属性)
- 返回新对象
-
object.create(proto, descriptors)
第一个参数是新创建对象的原型,第二个参数可选,把属性名映射到属性描述符
对象查询
可以通过点或者方括号来获取属性的值
const person={
age:18,
name:'lalala',
job:'worker'
};
console.log(person.age)
console.log(person["age"]) // 方括号里的表达式必须为字符串,或者是能转化为字符串的值,
let age = 'age';
console.log(person[age]) // 其实也可以是一个变量,还可以[`${param}111`]这种形式
在查询过程中,会先在自有属性中进行查询,自有属性中没有就会查询原型链,一直找到对应属性或者原型是null的对象为止。
属性赋值操作首先会检查原型链来判断属性是否允许赋值,就算允许,也是在原始对象上进行赋值操作或者创建新属性,而不会修改原型链,所以只有查询的时候才会体会到继承
对象删除
可以利用delete 运算符删除对象的属性,但是delete只是断开属性和宿主对象的联系,而不会去操作属性中的属性,这样的话已经删除的属性的引用依然存在,所以可能会存在内存泄漏
const a = {
b:{
c:'lalala'
}
}
let aa = a.b;
delete a.b; // 不能直接delete a;
console.log(a) // {}
console.log(aa) // {c:'lalala'}
delete只能删除自有属性,不能删除继承属性,不能删除可配置性为false的属性(比如通过变量声明和函数声明创建的全局对象的属性)
检测属性
- in运算符:左侧是属性名,右侧是对象,in可以区分不存在的属性和存在但是为null的属性
- hasOwnProperty():用来检测给定的名字是否是对象的自有属性,继承属性会返回false
- propertyIsEnumerable():只有检测到是自有属性且可枚举时才返回true
数组
js数组是js对象的特殊形式数组是值的有序集合,每一个值叫做集合,每个元素在数组中有一个位置,以数字表示,被称为索引(和对象中的属性类比一下),判断是否为数组可以用Array.isArray(arr)
.
常用的数组方法
-
栈和队列方法
- pop()
- push()
- shift()
- unshift()
-
操作方法
- concat() // 将多个数组或者多个值拼接起来,并返回一个新数组,如果什么都不传会返回一个复制的新数组 💖不会改变原数组
- slice(from, to) // 返回指定数组的片段或子数组,返回新数组,传一个0会返回一个复制的新数组💖不会改变原数组
- splice(start, length, […args]) // 从数组中插入或者删除元素,第一个参数是删除开始的下标,第二个参数是删除几个,填0的话表示一个都不删,忽略表示全部都删,第三个参数开始指定需要插入到数组中的元素,返回的是被删除的元素💖会改变原数组
- fill(value, from, to) // 批量填充某个值,第一个参数表示拿来填充的值,第二个参数表示开始填充的索引,默认为0,第三个参数表示结束索引,默认为数组长度,这个函数只能修改数组内容,并不能增加数组长度,如果value值为一个引用数据类型,则fill之后,数组里面的值指向的是同一个地址。如果改变了其中一个,则其它的都会改变
-
位置方法
- indexOf() // 如果没有,返回-1
- lastIndexOf() // 如果没有,返回-1
- includes() // 如果没有,返回false
-
迭代方法
-
every()
// 手写every函数 Array.prototype.every = function(fn){ if(typeof fn !== 'function'){ return new Error(`${fn} is not a function`) } for(let i = 0; i < this.length; i++){ if(!fn(this[i])){ return false } } return true };
-
some()
// 手写some Array.prototype.some = function(fn){ if(typeof fn !== 'function'){ return new Error(`${fn} is not a function`) } for(let i = 0; i < this.length; i++){ if(fn(this[i])){ return true } } return false };
-
forEach()
// 手写forEach Array.prototype.forEach = function(fn){ if(typeof fn !== 'function'){ return new Error(`${fn} is not a function`) } let res = []; for(let i = 0; i < this.length; i++){ fn(this[i],i,this) } };
-
map()
// 手写map Array.prototype.map = function(fn){ if(typeof fn !== 'function'){ return new Error(`${fn} is not a function`) } let res = []; for(let i = 0; i < this.length; i++){ res.push(fn(this[i],i,this)) } return res };
-
filter()
// 手写filter Array.prototype.filter = function(fn){ if(typeof fn !== 'function'){ return new Error(`${fn} is not a function`) } let res = []; for(let i = 0; i < this.length; i++){ fn(this[i]) && res.push(fn(this[i])) } return res };
-
find()
// 手写find Array.prototype.filter = function(fn){ if(typeof fn !== 'function'){ return new Error(`${fn} is not a function`) } for(let i = 0; i < this.length; i++){ if(fn(this[i])) { return this[i] } } return res };
-
-
归并方法
-
reduce()
arr.reduce(function(prev,cur,index,arr){ //TODO },init)
arr:原数组
prev:上一次调用回调时的返回值,或者初始的init
cur:当前正在处理的数组元素
index:当前正在处理的数组元素的索引,如果有init,则为0,否则为1;
init:初始值
一般而言,reduce主要用来处理数组内部的数据间的关系,比如累加,数组项比较大小,数组去重等等
? 如果有初始值,相当于从初始值出发进行遍历,从数组的第一个值开始计算,如果没有初始值,相当于直接处理第一个数和第二个数
2.基础用法
// 累加 let arr = [1,2,3,4,5]; arr.reduce((pre,curr)=>{ return pre+curr }) // 15 // 有初始值的累加 let arr = [1,2,3,4,5]; arr.reduce((pre,curr)=>{ return pre+curr },2) // 17,此时初始值就相当于是遍历的第一个值 // 求数组最大值,此时只是数据内部的比较,不需要设置初始值 let arr = [1,2,3,4,5]; arr.reduce((pre,curr)=>{ return Math.max(pre,curr) }) // 简单数组去重 let arr = [1,2,3,4,5,5,6,7]; arr.reduce((pre,curr)=>{ pre.indexOf(curr) === -1 && pre.push(curr) return pre },[]) // 初始化一个空数组,将处理之后的数据push到空数组中并返回 // 对象数组去重 let arr = [{key:1},{key:2},{key:3},{key:4},{key:5},{key:5},{key:6},{key:7}]; let obj = {}; arr.reduce((pre,curr)=>{ obj[curr.key] ? '': obj[curr.key]=true && pre.push(curr) return pre },[]) // 根据数组对象中的唯一性属性进行判断,例子是key,借助obj对象来判断key值是否已经存在,如果不存在,则push,否则跳过
// 手写reduce Array.prototype.reduce = function(fn, prev){ for(let i =0; i < i.length; i++){ if(typeof prev === 'undefined'){ prev = fn(this[i],this[i+1],i+1, this); ++i; }else { prev = fn(prev, this[i],i, this) } } return prev }
-
reduceRight()
-
-
排序方法
-
sort()
默认按升序排列,会调用每个数组项的toString方法,然后比较得到的字符串,也接收一个比较函数
// 快排方式 const quickSort = (arr,s,e)=>{ if(arr.length<=1) return if(s>=e) return let p = arr[s] let i = s let j = e while(i!=j){ while(arr[j]>=p&&i<j){ j-- } while(arr[i]<=p&&i<j){ i++ } if(i<j){ let temp = arr[i] arr[i] = arr[j] arr[j] = temp } } arr[s] = arr[i] arr[i] = p console.log(s,e,p,arr[i] ) quickSort(arr,s,i-1) quickSort(arr,i+1,e) }
-
reverse() // 直接将数组反序
// 手写reverse Arrray.prototype.reverse = function(fn){ for(let i = 0; i < this.length/2; i++){ let temp = this[i]; this[i] = this[this.length - i -1]; this[this.length - i -1] = temp } return this }
-
-
其他数组处理方法
-
join()
-
flat()
-
// 手写flat
function flat(arr,d=1){
return d > 0 ? arr.reduce((pre,curr)=>pre.concat(Array.isArray(curr)? flat(curr,d-1): curr),[]):arr.slice()
}
// https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Array/flat
- flatMap() // 首先使用映射函数映射每个元素,然后将结果压缩成一个新数组