跳至主要內容

面向对象

zfh大约 16 分钟约 4768 字

对象

  • 对象(object)是“键值对”的集合,表示属性和值的映射关系
  • 如果对象的属性键名不符合标识符命名规范,则这个键名必须用引号包裹
var frank = {
  name: 'frank',
  'favorite-song': 'see-you-again',
}

属性的访问

in 运算符

检查属性是否存在的操作符 "in"。

语法是:"key" in object 例如:

let user = { name: 'John', age: 30 }

alert('age' in user) // true,user.age 存在
alert('blabla' in user) // false,user.blabla 不存在。
  • 普通属性名使用点语法来访问

  • 如果属性名不符合标识符命名规范,则必须用方括号的写法来访问

frank['favorite-song'] //‘see-you-again’
  • 如果属性名以变量形式存储,则必须使用方括号形式

提示

虽然可以采用纯数字作为 key,但这本身就是不合法的标识符命名,所以访问需要使用方括号语法

var obj = {
  a: 1,
  b: 2,
}
var key = 'b' // 属性名用变量存储
console.log(obj.key) // undefined
console.log(obj[key]) // 2
  • 可选链:?.:是一种访问嵌套对象属性的安全的方式。即使中间的属性不存在,也不会出现错误
    • 如果可选链?. 前面的部分是 undefined 或者 null,它会停止运算并返回该部分
    • 可选链?.语法有三种形式:
      • obj?.prop —— 如果 obj存在则返回 obj.prop,否则返回 undefined
      • obj?.[prop] —— 如果 obj存在则返回obj[prop],否则返回 undefined
      • obj.method?.() —— 如果 obj.method 存在则调用 obj.method(),否则返回 undefined

属性的更改

直接使用赋值运算符重新对某属性赋值即可更改属性

var obj = {
  a: 10,
}
obj.a = 30
obj.a++

属性的创建

如果对象本身没有某个属性值,则用点语法赋值时,这个属性会被创建出来

var obj = {
  a: 10,
}
obj.b = 30
console.log(obj.b) // 30

属性的删除

使用 delete操作符

var obj = {
  a: 1,
  b: 2,
}
delete obj.a

对象的方法

  • 如果某个属性值是函数,则它也被称为对象的 方法
  • 使用点语法可以调用对象的方法
  • 方法也是函数,只不过方法是对象的 函数属性,它需要用对象打点调用

对象的遍历

遍历对象需要使用 for...in循环

// k 循环变量,它会依次成为对象的每一个键
for (var k in obj) {
  console.log('属性' + k + '的值是' + obj[k])
}

一般情况下,for...in 循环只会遍历我们自定义的属性,原型上默认的属性不会遍历出来。例如 Object.prototype.toString()Object.prototype.hasOwnProperty()是不会被遍历出来的。

但在实际应用中,如果是在原型中新增属性或者方法for...in 会将原型中新增的属性和方法遍历出来。

const obj = {
  a: 1,
  b: 2,
}
Object.prototype.c = 3
for (var x in obj) {
  console.log(x, obj[x])
  // a 1
  // b 2
  // c 3
}

所以我们不能依赖于 for...in 来获取对象的成员名称,一般使用 hasOwnProperty 来判断下

const obj = {
  a: 1,
  b: 2,
}
Object.prototype.c = 3
for (var x in obj) {
  if (obj.hasOwnProperty(x)) {
    console.log(x, obj[x])
    // a 1
    // b 2
  }
}

用它循环对象,循环出来的属性顺序并不可靠,所以不要在for...in中做依赖对象属性顺序的逻辑判断

JavaScript for...in 循环出来的对象属性顺序到底是什么规律?open in new window

JavaScript for...in循环出来的对象属性顺序到底是什么规律?

先遍历出整数属性(integer properties,按照升序),然后其他属性按照创建时候的顺序遍历出来。

let codes = {
  49: 'Germany',
  41: 'Switzerland',
  44: 'Great Britain',
  1: 'USA',
}

for (let code in codes) {
  alert(code) // 1, 41, 44, 49
}

最终遍历出来的结果是:属性 1 先遍历出来, 49 最后遍历出来。

这里的 1、41、44 和 49 就是整数属性。

那什么是整数属性呢?我们可以用下面的比较结果说明:

String(Math.trunc(Number(prop)) === prop // 当判断结果为 true,prop 就是整数属性,否则不是。

所以

  • "49" 是整数属性,因为 String(Math.trunc(Number('49')) 的结果还是 "49"。
  • "+49" 不是整数属性,因为 String(Math.trunc(Number('+49')) 的结果是 "49",不是 "+49"。
  • "1.2" 不是整数属性,因为 String(Math.trunc(Number('1.2')) 的结果是 "1",不是 "1.2"。

上面的例子中,如果想按照创建顺序循环出来,可以用一个 讨巧 的方法:

let codes = {
  '+49': 'Germany',
  '+41': 'Switzerland',
  '+44': 'Great Britain',
  // ..,
  '+1': 'USA',
}

for (let code in codes) {
  console.log(+code) // 49, 41, 44, 1
}

原型中新增的属性或方法,总是在最后按照顺序打印

const obj = {
  3: 'xx',
  1: 'frank',
  2: 'chang',
  name: 'zfh',
  age: 18,
}
Object.prototype[7] = 'zhang'
Object.prototype[6] = 'frank1'
for (var k in obj) {
  console.log('属性' + k + '的值是' + obj[k])
}
// 属性1的值是frank
// 属性2的值是chang
// 属性3的值是xx
// 属性name的值是zfh
// 属性age的值是18
// 属性6的值是frank1
// 属性7的值是zhang

对象的深浅克隆

JS 深拷贝总结open in new window

对象是引用类型值,这意味着:

不能用 var obj2=obj1这样的语法克隆一个对象。使用或者=进行对象的比较时,比较的是它们是否为内存中的同一个对象,而不是比较值是否相同。

var obj1 = {
  a: 1,
  b: 2,
  c: [1, 23, 4123],
}
var obj2 = {}
for (var k in obj1) {
  obj2[k] = obj1[k]
}
console.log(obj2.c === obj2.a) // true,浅克隆不可隆属性值为引用类型的键

JS 的原生不支持深拷贝,上面代码使用for...in,还有Object.assign{...obj}都属于浅拷贝;数组可以利用Array.prototype.concat(),Array.prototype.slice()实现浅拷贝。

JSON.sringify 和 JSON.parse 可以实现深拷贝,原理就是先将对象转换为字符串,再通过 JSON.parse 重新建立一个对象。 但是这种方法的局限也很多:

  • 不能复制 function、正则、Symbol
let obj = {
  reg: /^asd$/,
  fun: function () {},
  syb: Symbol('foo'),
  asd: 'asd',
}
let cp = JSON.parse(JSON.stringify(obj))
console.log(cp) // { reg: {}, asd: 'asd' },可以看到,函数、正则、Symbol 都没有被正确的复制.
  • 循环引用报错,当对象 1 中的某个属性指向对象 2,对象 2 中的某个属性指向对象 1 就会出现循环引用
function circularReference() {
  let obj1 = {}
  let obj2 = {
    b: obj1,
  }
  obj1.a = obj2
}

对包含循环引用的对象(对象之间相互引用,形成无限循环)执行 JSON.stringify()open in new window,会抛出错误

  • 相同的引用会被重复复制
let obj = { asd: 'asd' }
let obj2 = { name: 'aaaaa' }
obj.ttt1 = obj2
obj.ttt2 = obj2
let cp = JSON.parse(JSON.stringify(obj))
obj.ttt1.name = 'change'
cp.ttt1.name = 'change'
console.log(obj, cp)

在原对象 obj 中的 ttt1 和 ttt2 指向了同一个对象 obj2,那么我在深拷贝的时候,就应该只拷贝一次 obj2 ,下面我们看看运行结果:

相同的引用会被重复复制
相同的引用会被重复复制

我们可以看到(上面的为原对象,下面的为复制对象),原对象改变 ttt1.name 也会改变 ttt2.name ,因为他们指向相同的对象。

但是,复制的对象中,ttt1 和 ttt2 分别指向了两个对象。复制对象没有保持和原对象一样的结构。因此,JSON 实现深复制不能处理指向相同引用的情况,相同的引用会被重复复制。

递归实现深拷贝,对于简单类型,直接复制。对于引用类型,递归复制它的每一个属性

/**
 * 实现的深拷贝仅仅是解决了深拷贝的关键问题,还需要针对不同的数据类型进行完善
 */

function deepClone(o) {
  // 判断是否是数组
  if (Array.isArray(o)) {
    var result = [] //此数组解决了循环引用和相同引用的问题,它存放已经递归到的目标对象
    for (let k = 0; k < o.length; k++) {
      result.push(deepClone(o[k]))
    }
    // 来到这里的都是对象
  } else if (typeof o === 'object') {
    var result = {}
    for (var k in o) {
      result[k] = deepClone(o[k])
    }
  } else {
    var result = o
  }
  return result
}

this

thisJavaScript中的一个关键字,它通常用于引用当前执行上下文中的对象。this 的值是在运行时确定的,取决于函数调用的方式和上下文环境

以下是 this 关键字可能指向的情况的总结:

  1. 全局作用域中的 this:在全局作用域中,this 关键字指向全局对象,例如在浏览器环境中指向 window 对象,在 Node.js 环境中指向 global 对象
  2. 函数中的 this
    • 独立函数调用中的 this:在独立函数调用中,this 关键字指向全局对象。但是在严格模式下,独立函数调用中的 this 将是 undefined
    • 对象方法中的 this:在对象方法中,this 关键字指向调用该方法的对象
    • callapply 方法中的 this:通过使用 callapply 方法,可以将 this 指定为方法的第一个参数,这样 this 就会指向该参数所表示的对象
    • bind 方法中的 this:使用 bind 方法可以创建一个新函数,并将 this 绑定到指定的值
    • 构造函数中的 this:在使用 new 关键字创建对象时,构造函数中的 this 指向正在创建的新对象
    • 延时器、定时器中的this:调用非箭头函数,指向window
    • 事件处理函数中的this:(非箭头函数)中的this是绑定事件的DOM元素
  3. 箭头函数中的 this:箭头函数的 this 值继承自外层作用域中的 this 值,而不是根据函数调用的方式动态绑定

构造函数

  • 用 new 调用一个函数,这个函数就被称为“构造函数”,任何函数都可以是构造函数,只需要用 new 调用它
  • 顾名思义,构造函数用来“构造新对象”,它内部的语句将为新对象添加若干属性和方法,完成对象的初始化
  • 构造函数必须用 new 关键字调用,否则不能正常工作,正因如此,开发者约定构造函数命名时首字母要大写
  • 使用 new 调用构造函数,会执行以下操作:

1)在内存中创建一个新对象

2)将新对象与构造函数通过原型链连接起来

3)将构造函数中的this 绑定到新对象上

4)执行构造函数内部的代码

5)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象

function Fun(a) {
  this.a = a
  return { [a]: 1 }
}
const obj = new Fun(2)
console.log(obj) // { '2': 1 }而不是 { a: 2 }

原型原型链

prototype

prototype
prototype
  • 任何函数都有prototype属性, prototype是英语“原型“的意思

  • prototype属性值是个对象,它默认拥有 constructor属性指回函数(所有对象都有constructor 属性(使用 Object.create(null) 创建的对象除外)此属性的值是对函数本身的引用)

function sum(a, b) {
  return a + b
}
console.log(sum.prototype)
console.log(sum.prototype.constructor === sum) // true
  • 普通函数来说的prototype属性没有任何用处,而构造函数的prototype属性非常有用
  • 构造函数的 prototype 属性是它的实例的原型

构造函数的prototype属性是它的实例的原型 注: __proto__:每个对象都有一个proto,可称为隐式原型

代码实现:

function Fun(a, b) {
  this.a = a
  this.b = b
}
var o = new Fun(1, 2)
console.log(Fun.prototype === o.__proto__) //true

原型链查找

实例可以打点访问它的原型的属性和方法,这被称为“原型链查找

如果实例化出来的对象已经有了原型上的同名属性,那么就不会进行原型链查找

hasOwnProperty

检查对象是否真正“自己拥有某属性或者方法"

function Fun(a, b) {
  this.a = a
  this.b = b
}
Fun.prototype.c = '5'
var o = new Fun(1, 2)
console.log(o.c) //5
console.log(o.hasOwnProperty('a')) //true
console.log(o.hasOwnProperty('c')) //false

instanceof 运算符

instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。

object instanceof constructor //object 某个实例对象 constructor 某个构造函数

in 运算符

in 只能检查某个属性或方法是否可以被对象访问,不能检查是否是自己的属性或方法

function Fun(a, b) {
  this.a = a
  this.b = b
}
Fun.prototype.c = '5'
var o = new Fun(1, 2)
console.log(o.c) //5
console.log('a' in o) //true
console.log('c' in o) //true

getPrototypeOf

Object.getPrototypeOf() 方法返回指定对象的原型

const obj = {}
console.log(Object.getPrototypeOf(obj) === Object.prototype)
// expected output: true

Object.prototype.isPrototypeOf()

isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上

Object.create()

Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的proto

const person = {
  isHuman: false,
  printIntroduction: function () {
    console.log(`My name is ${this.name}. Am I human? ${this.isHuman}`)
  },
}

const me = Object.create(person)

me.name = 'Matthew' // "name" is a property set on "me", but not on "person"
me.isHuman = true // inherited properties can be overwritten

me.printIntroduction()
// expected output: "My name is Matthew. Am I human? true"

在 prototype 上添加方法

之前是把方法直接添加到实例身上的缺点:每个实例和每个实例的方法函数都是内存中不同的函数,造成了内存的浪费,解决办法:将方法写到 prototype 上

function Fun(a, b) {
  this.a = a
  this.b = b
}
Fun.prototype.sum = function (x) {
  return this.a + this.b + x
}
var o = new Fun(1, 2)
console.log(o.sum(3)) //6

原型链的终点

原型链的终点是 Object.prototype,所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

如何理解JS原型
如何理解JS原型

实现继承

// TODO 待改

function People(name, age, sex) {
  this.name = name
  this.age = age
  this.sex = sex
}
People.prototype.sayhello = function () {
  console.log('你好' + '我是' + this.name)
}
People.prototype.sleep = function () {
  console.log('我要睡觉!!')
}
function Student(name, age, sex, school, classNumber) {
  this.name = name
  this.age = age
  this.sex = sex
  this.school = school
  this.class = classNumber
}
// 实现继承
Student.prototype = new People()
// 必须在下边代码之前
Student.prototype.study = function () {
  console.log(this.school + '都是好学生')
}
Student.prototype.exam = function () {
  console.log(this.name + '考的不错')
}
var frank = new Student('frank', 22, '男', 'SNUT', '1')

原型链继承的缺点:

  1. 原型中包含的引用值会在所有实例间共享,这也是为什么属性通常会在构造函数中定义而不会定义在原型上的原因。在使用原型实现继承时,原型实际上变成了另一个类型的实例。这意味着原先的实例属性摇身一变成为了原型属性。

  2. 子类型在实例化时不能给父类型的构造函数传参。再加上之前提到的原型中包含引用值的问题,就导致原型链基本不会被单独使用。

面向对象

面向对象的本质:定义不同的类,让类的实例工作

面向对象的优点:程序编写更清晰、代码结构更严密、使代码更健壮更利于维护

面向对象经常用到的场合:需要封裝和复用性的场合(组件思维)

内置对象

基本包装类型

  • Number()、 String()和 Boolean()分别是数字、字符串布尔值的“包装类"----说白了就是构造函数!

  • 包装类的目的就是为了让基本类型值可以从它们的构造函数的 prototype 上获得方法

  • Number()、 String()和 Boolean()的实例都是 object 类型它们的 Primitivevalue 属性存储它们的本身值

  • new 出来的基本类型值可以正常参与运算

Math 对象

Math 是一个内置对象,它拥有一些数学常数属性和数学函数方法。Math 不是一个函数对象。

幂和开方

Math.pow(),Math.sqrt()

Math.pow(2, 3) // 8
Math.pow(3, 2) // 9
Math.sqrt(81) // 9
Math.sqrt(-81) // NaN

Math.pow()现在有了自己的操作符:**,Math.pow(2, 3)和2**3是一样的

向上取整向下取整

Math.ceil()向上取整;Math.floor()向下取整

console.log(Math.ceil(123.1)) // 124
console.log(Math.floor(123.1)) // 123

有关 IEEE754

  • 在 JavaScript 中,有些小数的数学运算不是很精准:0.1+0.2 不等于 0.3

  • Javascript 使用了 IEEE754 二进制浮点数算术标准,这会使一些个别的小数运算产生“丢失精度”问题,解决办法详见 为什么 0.1+0.2!==0.3

四舍五入 Math.round()

Math.round(),返回一个数字四舍五入后最接近的整数

如何四舍五入到小数点后两位?

四舍五入到小数点某位
四舍五入到小数点某位
// 四舍五入到小数点后两位
var n1 = 3.1231
var n2 = Math.round(n1 * 100) / 100
console.log(n2) //3.12

Math.max()和 Math.min()

  • Math.max()可以得到参数列表的最大值

-Math.min()可以得到参数列表的最小值

如果有任一参数不能被转换为数值,结果为 NaN

  • 如何利用 Math.max()求数组最大值?

Math.max()要求参数必须是“罗列出来”,而不能是数组

利用 apply 方法,它可以指定函数的上下文,并且以数组的形式传入“零散值”当做函数的参数

var arr = [1, 23, 444, 4, 4, 41, 12312312]
console.log(Math.max.apply(null, arr)) //123212312

这里并没有使用 apply 指定函数上下文所以用 null,表示空对象,而是利用它可以以数组的形式传入“零散值”当做函数的参数

随机数 Math.random()

parseInt 是用于字符串,而不是用于数字open in new window

js 生成[n,m]的随机数open in new window

可以得到 0 到 1 的随机小数

为了得到[a,b]区间内的整数,可以使用 这个公式:

// 这里也可以使用Math.trunc()
Math.floor(Math.random() * (b - a + 1)) + a

Math.trunc()

Math.trunc()方法会将数字的小数部分去掉,只保留整数部分。

不像 Math 的其他三个方法:Math.floor()Math.ceil()Math.round()Math.trunc() 的执行逻辑很简单,仅仅是删除掉数字的小数部分和小数点,不管参数是正数还是负数

Date 对象

  • 使用 new Date()即可得到当前时间的日期对象,它是 Object 类型值
  • 使用 new date(2020,11,1)即可得到指定日期的日期对象,注意第二个参数表示月份,从 0 开始算,11 表示 12 月
  • 也可以是new Date('2020-12-01')这样的写法

日期对象常见方法

日期对象常见方法
日期对象常见方法

时间戳

  • 时间戳表示 1970 年 1 月 1 日零点整距离某时刻的毫秒数
  • 通过 getTime()实例方法 或者Date.parse()可以将日期对象变为时间戳
var day = new Date('2021-1-12')

console.log(day.getTime()) // 1610380800000

console.log(Date.parse(day)) //1610380800000
  • 通过 new date(时间戳)的写法,可以将时间戳变为日期对象
上次编辑于:
本站勉强运行 小时