对象

基础知识

对象是包括属性与方法的数据类型,JS中大部分类型都是对象如 String/Number/Math/RegExp/Date 等等。

传统的函数编程会有错中复杂的依赖很容易创造意大利式面条代码。

  • 面向过程编程
const name = 'mkimq'

const grade = [
	{
		lesson: 'js',
		score: 60
	},
	{
		lesson: 'css',
		score: 61
	}
]

function average(grade, name) {
	const total = grade.reduce((t, a) => t + a.score, 0)

	return `${name}: ${total / grade.length}`
}

console.log(average(grade, name))
  • 面向对象编程

下面使用对象编程的代码结构清晰,也减少了函数的参数传递,也不用担心函数名的覆盖

const user = {
	name: 'mkimq',
	grade: [
		{
			lesson: 'js',
			score: 60
		},
		{
			lesson: 'css',
			score: 61
		}
	],
	average() {
		const total = this.grade.reduce((t, a) => t + a.score, 0)

		return `${this.name}: ${total / this.grade.length}`
	}
}

console.log(user.average())

OOP

  • 对象是属性和方法的集合即封装
  • 将复杂功能隐藏在内部,只开放给外部少量方法,更改对象内部的复杂逻辑不会对外部调用造成影响即抽象
  • 继承是通过代码复用减少冗余代码
  • 根据不同形态的对象产生不同结果即多态

基本声明

使用字面量形式声明对象是最简单的方式

const obj = {
    name: 'mkimq',
    get: function() {
        return this.name
    }
}

console.log(obj.get())

属性与方法简写

const name = 'mkimq'

const obj = {
    name,
    get() {
        return this.name
    }
}

console.log(obj.get())

其实字面量形式在系统内部也是使用构造函数 new Object创建的。

let mk = {}
let mk2 = new Object()
console.log(mk, mk2)
console.log(mk.constructor)
console.log(mk2.constructor)

操作属性

使用点语法获取

const user = {
    name: 'mkimq'
}

user.name

使用[]获取

user['name']

可以看出使用.操作属性更简洁,[]主要用于通过变量定义属性的场景 如果属性名不是合法变量名就必须使用[]的形式了

对象和方法的属性可以动态的添加或删除。

const mk = {
    name: 'mkimq'
}

mk.age = 18
mk.show = function() {
    return `${this.name}已经${this.age}岁了`
}

delete mk.age
delete mk.show

对象方法

定义在对象中的函数我们称为方法。

引用特性

  • 对象和函数、数组一样是引用类型,即复制只会复制引用地址。
  • 对象做为函数参数使用时也不会产生完全赋值,内外共用一个对象
  • 对多的比较是对内存地址的比较所以使用 == 或 === 一样

this

this 指当前对象的引用,始终建议在代码内部使用this 而不要使用对象名,不同对象的this只指向当前对象。

展开语法

使用...可以展示对象的结构

对象转换

基础知识

对象直接参与计算时,系统会根据计算的场景在 string/number/default 间转换。

  • 如果声明需要字符串类型,调用顺序为 toString > valueOf
  • 如果场景需要数值类型,调用顺序为 valueOf > toString
  • 声明不确定时使用 default ,大部分对象的 default 会当数值使用

下面的数值对象会在数学运算时转换为 number

const m = new Number(1)
console.log(m + 3)

如果参数字符串运算时会转换为 string

const m = new Number(1)
console.log(m + '3') // '13'

下面当不确定转换声明时使用 default ,大部分default转换使用 number 转换。

const m = new Number(1)
console.log(m == '1')

Symbol.toPrimitive

内部自定义Symbol.toPrimitive方法用来处理所有的转换场景

const m = {
	num: 1,
	[Symbol.toPrimitive]: function() {
		return this.num
	}
}

console.log(m + 3)

valueOf/toString

可以自定义valueOf 与 toString 方法用来转换,转换并不限制返回类型。

const m = {
	name: 'mkimq',
	num: 1,
	valueOf: function () {
		console.log('valueOf')
		return this.num
	},
	toString: function() {
		console.log('toString')
		return this.name
	}
}

console.log(m + 3)
console.log(`${m}-mankeung`)

解构赋值

解构是一种更简洁的赋值特性,可以理解为分解一个数据的结构,在数组章节已经介绍过。

  • 建设使用 var/let/const 声明

基本使用

下面是基本使用语法

const info = {
    name: 'mkimq',
    url: 'mkimq.com'
}

const {name, url: site} = info
console.log(name, site)

函数返回值直接解构到变量

function fn() {
    return {
        name: 'mkimq',
        url: 'mkimq.com'
    }
}

const {name, url} = fn()

函数传参

function fn({name, age}) {
    console.log(name, age)
}

fn({name: 'mkimq', age: 18})

严格模式

非严格模式可以不使用声明指令,严格模式下必须使用声明。所以建议使用 let 等声明。

({name, url} = {name: 'mkimq', url: 'mkimq.com'})

还是建议使用let等赋值声明

"use strict";

const {name, url} = {name: 'mkimq', url: 'mkimq.com'}

简洁定义

如果属性名与赋值的变量名相同可以更简洁

const web = {name: 'mkimq', url: 'mkimq.com'}
const {name, url} = web

只赋值部分变量

const [, url] = ['mkimq', 'mkimq.com']
const {name} = {name: 'mkimq', url: 'mkimq.com'}

可以直接使用变量赋值对象属性

const name = 'mkimq'
const url = 'mkimq.com'

const mk = {name, url}

嵌套解构

可以操作多层复杂数据结构

默认值

为变量设置默认值

const [name, url = 'mkimq.com'] = ['mkimq']

const {name, age = 18} = {name: 'mkimq'}

使用默认值特性可以方便的对参数预设

函数参数

数组参数的使用

function fn([a, b]) {
    console.log(a, b)
}

对象参数使用方法

function fn({a, b}) {
    console.log(a, b)
}

对象解构传参

function user(name, {sex, age} = {}) {
    console.log(name, sex, age)
}

属性管理

添加属性

可以为对象添加属性

const obj = {}
obj.age = 18

删除属性

使用delete 可以删除属性

const obj = {
    age: 18
}

delete obj.age

检测属性

hasOwnProperty检测对象自身是否包含指定的属性,不检测原型链上继承的属性。

const obj = {
    age: 18
}

console.log(obj.hasOwnProperty('age'))

使用 in 可以在原型对象上检测

const obj = {
    age: 18
}

const mk = {
    name: 'mkimq'
}

// 设置mk为obj的新原型
Object.setPrototypeOf(obj, mk)

console.log(obj)

console.log('name' in obj)
console.log(obj.hasOwnProperty('name'))

获取属性名

使用 Object.getOwnPropertyNames 可以获取对象的属性名集合

const obj = {
    name: 'mkimq',
    age: 18
}

const names = Object.getOwnPropertyNames(obj)

console.log(names)

assign

以往我们使用类似jQuery.extend 等方法设置属性,现在可以使用 Object.assign 静态方法

let obj = {}

obj = Object.assign(obj, {name: 'mkimq'}, {age: 18})
console.log(obj)

计算属性

对象属性可以通过表达式计算定义,这在动态设置属性或执行属性方法时很好用。

let id = 0

const user = {
    [`id-${id}`]: id,
    [`id-${id}`]: id,
    [`id-${id}`]: id
}

传值操作

对象是引用类型赋值是传址操作

const user = {
    name: 'mkimq'
}

const mk = {
    user
}

mk.user.name = 'MKIMQ'
console.log(user.name)

遍历对象

获取内容

使用系统提供的API可以方便获取对象属性与值

const obj = {
    name: 'mkimq',
    age: 18
}

console.log(Object.keys(obj))
console.log(Object.values(obj))
console.log(Object.entries(obj))

for/in

使用for/in遍历对象属性

const obj = {
    name: 'mkimq',
    age: 18
}

for (const key in obj) {
    console.log(key, obj[key])
}

for/of

for/of用于遍历迭代对象,不能直接操作对象。但Object对象的keys/方法返回的是迭代对象。

const obj = {
    name: 'mkimq',
    age: 18
}

for (const key of Object.keys(obj)) {
    console.log(key)
}

获取所有对象属性

for (const key of Object.values(obj)) {
    console.log(key)
}

同时获取属性名与值

for (const arr of Object.entries(obj)) {
    console.log(arr)
}

使用扩展语法同时获取属性名与值

for (const [key, value] of Object.entries(obj)) {
    console.log(key, value)
}

对象拷贝

对象赋值时复制的内存地址,所以一个对象的改变直接影响另一个

浅拷贝

使用for/in执行对象拷贝

const obj = {
    name: 'mkimq',
    age: 18
}

const obj2 = {}

for (const key in obj) {
    obj2[key] = obj[key]
}

obj2.name = 'MKIMQ'

console.log(obj)
console.log(obj2)

Object.assign 函数可简单的实现浅拷贝,它是将两个对象的属性叠加后面对象属性会覆盖前面对象同名属性。

const obj = {
    name: 'mkimq',
    age: 18
}

const obj2 = Object.assign({}, obj)

使用展示语法也可以实现浅拷贝

const obj2 = {...obj}

深拷贝

浅拷贝不会将深层的数据复制

const obj = {
	name: 'mkimq',
	user: {
		name: 'mankeung'
	}
}

function copy(object) {
	let obj = {}

	for (const key in object) {
		obj[key] = object[key]
	}

	return obj
}

const newObj = copy(obj)

newObj.name = 'MKIQM'
newObj.user.name = 'MANKEUNG'

console.log(newObj)
console.log(obj)

是完全的复制一个对象,两个对象是完全独立的对象

const obj = {
	name: 'mkimq',
	user: {
		name: 'mankeung'
	},
	data: []
}

function copy(object) {
	let obj = object instanceof Array ? [] : {}

	for (const [k, v] of Object.entries(object)) {
		obj[k] = typeof v == 'object' ? copy(v) : v
	}

	return obj
}

const newObj = copy(obj)

newObj.data.push('wwq')

console.log(JSON.stringify(newObj, null, 2))
console.log(JSON.stringify(obj, null, 2))

构建函数

对象可以通过内置或自定义的构造函数创建。

工厂函数

在函数中返回对象的函数称为工厂函数,工厂函数有以下优点

  • 减少重复创建相同类型对象的代码
  • 修改工厂函数的方法影响所有同类对象

使用字面量创建对象需要复制属性与方法结构

const mk = {
    name: 'mk',
    show() {
        console.log(this.name)
    }
}

const lisi = {
    name: 'lisi',
    show() {
        console.log(this.name)
    }
}

使用工厂函数可以简化这个过程

function fn(name) {
    return {
        name,
        show() {
            console.log(this.name)
        }
    }
}

const lisi = fn('lisi')
lisi.show()

const mk = fn('mk')
mk.show()

构造函数

和工厂函数相似构造函数也用于创建对象,它的上下文为新的对象实例。

  • 构造函数名每个单词首字母大写即Pascal 命名规范
  • this指当前创建的对象
  • 不需要返回this系统会自动完成
  • 需要使用new关键词生成对象
function Student(name) {
    this.name = name

    this.show = function() {
        console.log(this.name)
    }
}

const lisi = new Student('lisi')
lisi.show()

如果构造函数返回对象,实例化后的对象将是此对象

function ArrayObject(...values) {
    const arr = new Array()

    arr.push.apply(arr, values)

    arr.string = function(sym = '|') {
        return this.join(sym)
    }

    return arr
}

const arr = new ArrayObject(1, 2, 3)
console.log(arr)
console.log(arr.string('-'))

严格模式

在严格模式下方法中的this值为undefined,这是为了防止无意的修改window对象

"use strict";

function User() {
	this.show = function () {
		console.log(this)
	}
}
let mk = new User()
mk.show() //User

let f = mk.show
f() //undefined

内置构造

JS中大部分数据类型都是通过构造函数创建的。

const num = new Number(99)
console.log(num.valueOf())

const string = new String('mkimq')
console.log(string.valueOf())

const boolean = new Boolean(true)
console.log(boolean.valueOf())

const date = new Date()
console.log(date.valueOf() * 1)

const regexp = new RegExp('\\d+')
console.log(regexp.test(99))

let mk = new Object()
mk.name = 'mkimq'
console.log(mk)

字面量创建的对象,内部也是调用了Object构造函数

对象函数

在JS中函数也是一个对象

function fn(name) {}

console.log(fn.toString())
console.log(fn.length)

函数是由系统内置的 Function 构造函数创建的

下面是使用内置构造函数创建的函数

const User = new Function(`name`,`
    this.name = name
    this.show = function() {
        return this.name
    }
`
)

const lisi = new User('lisi')
console.log(lisi.show())

抽象特性

将复杂功能隐藏在内部,只开放给外部少量方法,更改对象内部的复杂逻辑不会对外部调用造成影响即抽象。

属性特征

查看特征

使用 Object.getOwnPropertyDescriptor查看对象属性的描述。

Object.getOwnPropertyDescriptor(user, 'name')

使用 Object.getOwnPropertyDescriptors查看对象所有属性的描述

Object.getOwnPropertyDescriptors(user)

属性包括以下四种特性

特性说明默认值
configurable能否使用delete、能否需改属性特性、或能否修改访问器属性true
enumerable对象属性是否可通过for-in循环,或Object.keys() 读取true
writable对象属性是否可修改true
value对象属性的默认值undefined

设置特征

使用Object.defineProperty 方法修改属性特性,通过下面的设置属性name将不能被遍历、删除、修改。

const user = {
    name: 'mkimq'
}

Object.defineProperty(user, 'name', {
    value: 'MKIMQ',
    writable: false,
    enumerable: false,
    configurable: false
})

// 使用 Object.defineProperties 可以一次设置多个属性,具体参数和上面介绍的一样。
Object.defineProperties(user, {
    name: {value: 'MKIMQ'},
    age: {value: 18}
})

禁止添加

Object.preventExtensions 禁止向对象添加属性

Object.preventExtensions(user)

user.sex = '男' // error

Object.isExtensible 判断是否能向对象中添加属性

Object.isExtensible(user) // true / false

封闭对象

Object.seal()方法封闭一个对象,阻止添加新属性并将所有现有属性标记为 configurable: false

const user = {
    name: 'mkimq',
    age: 18
}

Object.seal(user)
console.log(
    JSON.stringify(Object.getOwnPropertyDescriptors(user), null, 2)
)

// Object.isSealed 如果对象是密封的则返回 true,属性都具有 configurable: false。
console.log(Object.isSealed(user))

冻结对象

Object.freeze 冻结对象后不允许添加、删除、修改属性,writable、configurable都标记为false

Object.freeze(user)

// Object.isFrozen()方法判断一个对象是否被冻结
console.log(Object.isFrozen(user))

属性访问器

getter方法用于获得属性值,setter方法用于设置属性,这是JS提供的存取器特性即使用函数来管理属性。

  • 用于避免错误的赋值
  • 需要动态监测值的改变
  • 属性只能在访问器和普通属性任选其一,不能共同存在

getter/setter

向对是地用户的年龄数据使用访问器监控控制

const user = {
	data: {
		name: 'mkimq',
		age: null
	},
    set age(value) {
		if (typeof value != 'number' || value > 100 || value < 10) {
			throw new Error('年龄格式错误~')
		}

		this.data.age = value
	},
	get age() {
		return `年龄是:${this.data.age}`
	}
}

user.age = 100

console.log(user.age)

下面通过设置站网站名称与网址体验getter/setter批量设置属性的使用

const web = {
	name: 'mkimq',
	url: 'mkimq.com',
	get site() {
		return `${this.name}: ${this.url}`
	},
	set site(value) {
		[this.name, this.url] = value.split(',')
	}
}

web.site = '百度,baidu.com'

console.log(web.site)

访问器描述符

使用 defineProperty 可以模拟定义私有属性,从而使用面向对象的抽象特性。

function User(name, age) {
	const data = {name, age}

	Object.defineProperties(this, {
		name: {
			get() {
				return data.name
			},
			set(value) {
				if (value.trim() == '') throw new Error('无效用户~')
				data.name = value
			}
		},
		age: {
			get() {
				return data.age
			},
			set(value) {
				if (value.trim() == '') throw new Error('无效年龄~')
				data.age = value
			}
		}
	})
}

const mk = new User('mkimq', 18)
console.log(mk.name, mk.age)

闭包访问器

下面结合闭包特性对属性进行访问控制

  • 下例中访问器定义在函数中,并接收参数
  • 在get() 中通过闭包返回
  • 在set() 中修改了v,这会影响get()访问的闭包数据
const data = {
	name: 'mkimq'
}

for (const [key, value] of Object.entries(data)) {
	observer(data, key, value)
}

function observer(data, key, v) {
	Object.defineProperty(data, key, {
		get() {
			return v
		},
		set(newValue) {
			v = newValue
		}
	})
}

data.name = 'MKIMQ'
console.log(data.name)

代理拦截

代理(拦截器)是对象的访问控制,setter/getter 是对单个对象属性的控制,而代理是对整个对象的控制。

  • 读写属性时代码更简洁
  • 对象的多个属性控制统一交给代理完成
  • 严格模式下 set 必须返回布尔值

使用方法

"use strict";

const obj = {
	name: 'mkimq'
}

const proxy = new Proxy(obj, {
	get(taget, p) {
		return taget[p]
	},
	set(taget, p, value) {
		taget[p] = value
		return true
	}
})

proxy.name = 10
console.log(obj)

代理函数

如果代理以函数方式执行时,会执行代理中定义 apply 方法。

  • 参数说明:函数,上下文对象,参数

下面使用 apply 计算函数执行时间

function factorial(num) {
	return num == 1 ? 1 : num * factorial(num - 1)
}

const proxy = new Proxy(factorial, {
	apply(func, obj, args) {
		console.time('run')
		const num = func.apply(obj, args)
		console.timeEnd('run')

		return num
	}
})

console.log(proxy.apply(this, [3]))
console.log(proxy(100))

双向绑定

下面通过代理实现vue 等前端框架的数据绑定特性特性。

<body>
    <input type="text" v-model="title" />
    <input type="text" v-model="title" />
    <div v-bind="title"></div>
</body>
function View() {
	// 设置代理拦截
	const proxy = new Proxy(
		{},
		{
			get(obj, property) {},
			set(obj, property, value) {
				obj[property] = value

				document
					.querySelectorAll(`[v-model="${property}"], [v-bind="${property}"]`)
					.forEach(el => {
						el.innerHTML = value
						el.value = value
					})
			}
		}
	)

	// 初始化绑定元素事件
	this.run = function() {
		const els = document.querySelectorAll('[v-model]')
		els.forEach(item => {
			item.addEventListener('keyup', function() {
				proxy[this.getAttribute('v-model')] = this.value
			})
		})
	}
}

const view = new View()

view.run()
点击查看效果

表单验证

<body>
    <input type="text" validate rule="max:12,min:3" />
    <input type="text" validate rule="max:3,isNumber" />
</body>
body {
    padding: 50px;
    background: #34495e;
}
input {
    border: solid 10px #ddd;
    height: 30px;
}
.error {
    border: solid 10px red;
}
class Validate {
	max(value, len) {
		return value.length <= len
	}

	min(value, len) {
		return value.length >= len
	}

	isNumber(value) {
		return /^\d+$/.test(value)
	}
}

// 代理
function createProxy(target) {
	return new Proxy(target, {
		get(target, key) {
			return target[key]
		},
		set(target, key, el) {
			const rule = el.getAttribute('rule')
			const validate = new Validate()

			const state = rule.split(',').every(rule => {
				const info = rule.split(':')

				return validate[info[0]](el.value, info[1])
			})

			el.classList[state ? 'remove' : 'add']('error')

			return true
		}
	})
}

const nodes = createProxy(document.querySelectorAll('[validate]'))

nodes.forEach((item, i) => {
	item.addEventListener('keyup', function() {
		nodes[i] = this
	})
})
点击查看效果

JSON

  • json 是一种轻量级的数据交换格式,易于人阅读和编写。

  • 使用json 数据格式是替换 xml 的最佳方式,主流语言都很好的支持json 格式。所以 json 也是前后台传输数据的主要格式。

  • json 标准中要求使用双引号包裹属性,虽然有些语言不强制,但使用双引号可避免多程序间传输发生错误语言错误的发生。

  • 基本结构

const user = {
    name: 'mkimq',
    age: 18
}

user.name
  • 数组结构
const users = [
    {
        name: 'mkimq',
        age: 18
    }
]
  • 序列化
    • 根据第二个参数指定保存的属性
    • 第三个是参数用来控制TAB数量,如果字符串则为前导字符。
JSON.stringify(user)

为数据添加 toJSON 方法来自定义返回格式

const user = {
    name: 'mkimq',
    age: 18,
    toJSON: function() {
        return {
            n: this.name,
            a: this.age
        }
    }
}

console.log(JSON.stringify(user))

反序列化

JSON.parse(jsonStr)

使用第二个参数函数来对返回的数据二次处理

JSON.parse(jsonStr, (key, value) => {
    if (key === 'name') return `[推荐] ${value}`

    return value
})

Reflect

Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法

  • Reflect并非一个构造函数,所以不能通过new运算符对其进行调用
const queuedObservers = new Set()
const observe = fn => queuedObservers.add(fn)
const observable = obj => new Proxy(obj, {
	set
})

function set(target, key, value, receiver) {
	const result = Reflect.set(target, key, value, receiver)
	queuedObservers.forEach(observer => observer())
	return result
}

//监听
const person = observable({
	name: '张三',
	age: 20
})

function print() {
	console.log(`${person.name}, ${person.age}`)
}
observe(print)
person.name = '李四'
贡献者: mankueng