跳至主要內容

面向对象

Mr.Chen前端基石JS基础对象原型原型链大约 24 分钟约 7137 字

对象

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

属性的访问

::: tip 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’
  • 如果属性名以变量形式存储,则必须使用方括号形式

::: tip 提示 虽然可以采用纯数字作为 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循环出来的对象属性顺序到底是什么规律?

先遍历出整数属性(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

对象的深浅克隆

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

不能用 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 指向问题

彻底搞懂 this 指向open in new window

开发中很少直接在全局作用域下去使用 this(浏览器环境下,全局作用域中的 this 非严格模式下为 window),通常都是在函数中使用

在函数中this到底取何值,是在函数真正被调用执行的时候确定的,函数定义的时候确定不了,因为 this 的取值是执行上下文环境的一部分,每次调用函数,都会产生一个新的执行上下文环境

this 绑定规则

this 无非就是在函数调用时被绑定的一个对象,我们就需要知道它在不同的场景下的绑定规则:

  1. 默认绑定

什么情况下使用默认绑定呢?独立函数调用

独立的函数调用我们可以理解成函数没有被绑定到某个对象上进行调用

案例一:普通函数调用

  • 该函数直接被调用,并没有进行任何的对象关联;
  • 这种独立的函数调用会使用默认绑定,通常默认绑定时,函数中的 this 指向全局对象window
function foo() {
  console.log(this) // window
}

foo()

案例二:函数调用链(一个函数又调用另外一个函数)

所有的函数调用都没有被绑定到某个对象上

function test1() {
  console.log(this) // window
  test2()
}

function test2() {
  console.log(this) // window
  test3()
}

function test3() {
  console.log(this) // window
}
test1()

案例三:将函数作为参数,传入到另一个函数中

function foo(func) {
  func()
}

function bar() {
  console.log(this) // window
}

foo(bar)

稍微修改一下:

function foo(func) {
  func()
}

var obj = {
  name: 'why',
  bar: function () {
    console.log(this) // window
  },
}

foo(obj.bar)

结果依然是 window,原因非常简单,在真正函数调用的位置,并没有进行任何的对象绑定,只是一个独立函数的调用;

  1. 隐式绑定

另外一种比较常见的调用方式是通过某个对象进行调用的:也就是它的调用位置中,是通过某个对象发起的函数调用

案例一:通过对象调用函数

foo是通过 obj.foo()方式进行调用的

那么 foo 调用时this会隐式的被绑定到 obj 对象上

function foo() {
  console.log(this) // obj对象
}

var obj = {
  name: 'why',
  foo: foo,
}

obj.foo()

案例二:案例一的变化

我们通过 obj2 又引用了 obj1 对象,再通过 obj1 对象调用foo函数;

那么 foo 调用的位置上其实还是 obj1 被绑定了 this

function foo() {
  console.log(this) // obj1对象
}

var obj1 = {
  name: 'obj1',
  foo: foo,
}

var obj2 = {
  name: 'obj2',
  obj1: obj1,
}

obj2.obj1.foo()

案例三:隐式丢失

结果最终是 window,因为 foo 最终被调用的位置是 bar,而 bar 在进行调用时没有绑定任何的对象,也就没有形成隐式绑定;相当于是一种默认绑定

function foo() {
  console.log(this) // window
}

var obj1 = {
  name: 'obj1',
  foo: foo,
}

// 将obj1的foo赋值给bar
var bar = obj1.foo
bar()
  1. 显式绑定

隐式绑定有一个前提条件:

  • 必须在调用的对象内部有一个对函数的引用(比如一个属性);
  • 如果没有这样的引用,在进行调用时,会报找不到该函数的错误;
  • 正是通过这个引用,间接的将 this 绑定到了这个对象上;

如果我们不希望在 对象内部 包含这个函数的引用,同时又希望在这个对象上进行强制调用,该怎么做呢?

JavaScript 所有的函数都可以使用 callapply 以及 bind 显式指定 this

这三个函数的第一个参数都要求是一个对象,这个对象的作用是什么呢?就是给 this 准备的。

在调用这个函数时,会将 this 绑定到这个传入的对象上

因为上面的过程,我们明确的绑定了 this 指向的对象,所以称之为 显式绑定

::: tip call,apply,bind 三者区别

call apply bind 三者的用法和区别open in new window

call()apply()bind() 都是用来重定义 this 这个对象的

bind 返回的是一个新的函数,你必须调用它才会被执行 ,其余两个都是立即执行的

bindcall 参数形式一致,apply 需要把参数写到数组里 :::

有些时候,我们会调用一些 JavaScript 内置函数,或者一些第三方库中的内置函数

这些内置函数会要求我们传入另外一个函数;

我们自己并不会显示的调用这些函数,而且 JavaScript 内部或者第三方库内部会帮助我们执行;

这些函数中的 this 又是如何绑定的呢?

案例一:setTimeout

setTimeout 中会传入一个函数,这个函数中的 this 通常是 window

setTimeout(function () {
  console.log(this) // window
}, 1000)

为什么这里是 window 呢?

这个和 setTimeout 源码的内部调用有关;setTimeout 内部是通过 apply 进行绑定的 this 对象,并且绑定的是全局对象;

那如果我们想让这里的 this 不是 window 呢?比如我们希望点击 box 盒子延时 2 秒变为红色,那么我们就需要备份一下 this

var box = document.getElementById('box')
function bRed() {
  var self = this // 备份this
  setTimeout(function () {
    self.style.backgroundColor = 'red'
  }, 2000)
}
box.onclick = bRed

或者使用箭头函数:

var box = document.getElementById('box')
function bRed() {
  setTimeout(() => {
    this.style.backgroundColor = 'red'
  }, 2000)
}
box.onclick = bRed

案例二:数组的 forEach

forEach 中传入的函数打印的也是 Window 对象; 这是因为默认情况下传入的函数是自动调用函数(默认绑定);

var names = ['abc', 'cba', 'nba']
names.forEach(function (item) {
  console.log(this) // 三次window
})

当然我们可以通过 forEach 的第二个参数改变 this 指向

var names = ['abc', 'cba', 'nba']
var obj = { name: 'why' }
names.forEach(function (item) {
  console.log(this) // 三次obj对象
}, obj)

案例三:div 的点击

获取 box 元素节点,并且监听点击:

在点击事件的回调中,this 指向谁呢?box 对象; 这是因为在发生点击时,执行传入的回调函数被调用时,会将 box 对象绑定到该函数中;

var box = document.querySelector('.box')
box.onclick = function () {
  console.log(this) // box对象
}
  1. new绑定

见构造函数章节

  1. 规则优先级
  • 默认规则的优先级最低

毫无疑问,默认规则的优先级是最低的,因为存在其他规则时,就会通过其他规则的方式来绑定 this

  • 显示绑定优先级高于隐式绑定
function foo() {
  console.log(this)
}

var obj1 = {
  name: 'obj1',
  foo: foo,
}

var obj2 = {
  name: 'obj2',
  foo: foo,
}

// 隐式绑定
obj1.foo() // obj1
obj2.foo() // obj2

// 隐式绑定和显示绑定同时存在
obj1.foo.call(obj2) // obj2, 说明显式绑定优先级更高
  • new 绑定优先级高于隐式绑定
function foo() {
  console.log(this)
}

var obj = {
  name: 'why',
  foo: foo,
}

new obj.foo() // foo对象, 说明new绑定优先级更高
  • new 绑定优先级高于 bind

new 绑定和 callapply 是不允许同时使用的,所以不存在谁的优先级更高

function foo() {
  console.log(this)
}

var obj = {
  name: 'obj',
}

var foo = new foo.call(obj) //报错

但是 new 绑定可以和 bind 后的函数同时使用

function foo() {
  console.log(this)
}

var obj = {
  name: 'obj',
}

var bar = foo.bind(obj)
var foo = new bar() // 打印foo, 说明使用的是new绑定

优先级总结new 绑定 > 显示绑定(bind)> 隐式绑定 > 默认绑定

this 规则之外

  1. 忽略显式绑定

如果在显式绑定中,我们传入一个 null 或者 undefined,那么这个显示绑定会被忽略,使用默认规则:

function foo() {
  console.log(this)
}

var obj = {
  name: 'why',
}

foo.call(obj) // obj对象
foo.call(null) // window
foo.call(undefined) // window

var bar = foo.bind(null)
bar() // window
  1. 间接函数引用

另外一种情况,创建一个函数的 间接引用,这种情况使用默认绑定规则。

function foo() {
  console.log(this)
}

var obj1 = {
  name: 'obj1',
  foo: foo,
}

var obj2 = {
  name: 'obj2',
}

obj1.foo() // obj1对象
;(obj2.foo = obj1.foo)() // window

赋值(obj2.foo = obj1.foo)的结果是 foo 函数;foo 函数被直接调用,那么是默认绑定;

  1. ES6 箭头函数

箭头函数不使用 this 的四种标准规则(也就是不绑定 this),而是根据外层作用域来决定 this

箭头函数中的 this 指向open in new window

构造函数

  • 用 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属性指回函数

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

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原型

js 实现继承

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

面向对象案例

JS 是面向过程、面向对象还是基于对象?面向对象的代码体现open in new window

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

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

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

红绿灯案例

页面上做一个红绿灯,点击红灯就变黄,点击黄灯就变绿,点击绿灯就变回红灯;如果页面上有 100 个这样的红绿灯呢? ::: details 代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <div id="box"></div>
  </head>
  <body>
    <script>
      var box = document.getElementById('box')
      function TrafficLight() {
        this.color = 1
        this.init()
        this.changeColor()
      }
      TrafficLight.prototype.init = function () {
        this.dom = document.createElement('img')
        this.dom.src = './' + this.color + '.jpg'
        box.appendChild(this.dom)
      }
      TrafficLight.prototype.changeColor = function () {
        var self = this
        // 这里的this指向的是实例化出来的对象
        self.dom.onclick = function () {
          // 这里的this指向的是事件处理函数绑定的D0M元素
          // 如果直接使用this,DOM元素有color属性?对吧,理解了
          self.color++
          if (self.color == 4) {
            self.color = 1
          }
          self.dom.src = './' + self.color + '.jpg'
        }
      }

      var light1 = new TrafficLight()
      var light2 = new TrafficLight()
      var light3 = new TrafficLight()
      var light4 = new TrafficLight()
    </script>
  </body>
</html>

素材: 红绿灯素材/1红绿灯素材/2红绿灯素材/3 :::

炫彩小球小案例

::: details 代码

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      body {
        background-color: black;
      }

      .ball {
        position: absolute;
        border-radius: 50%;
      }
    </style>
  </head>

  <body>
    <script>
      // 小球类
      function Ball(x, y) {
        // 属性x、y表示的是圆心的坐标
        this.x = x
        this.y = y
        // 半径属性
        this.r = 20
        // 透明度
        this.opacity = 1
        // 小球背景颜色,从颜色数组中随机选择一个颜色
        this.color = colorArr[Math.floor(Math.random() * colorArr.length)]
        // 这个小球的x增量和y的增量,使用do while语句,可以防止dX和dY都是零
        do {
          this.dX = Math.floor(Math.random() * 20) - 10
          this.dY = Math.floor(Math.random() * 20) - 10
        } while (this.dX == 0 && this.dY == 0)

        // 初始化
        this.init()
        // 把自己推入数组,注意,这里的this不是类本身,而是实例
        ballArr.push(this)
      }
      // 初始化方法
      Ball.prototype.init = function () {
        // 创建自己的dom
        this.dom = document.createElement('div')
        this.dom.className = 'ball'
        this.dom.style.width = this.r * 2 + 'px'
        this.dom.style.height = this.r * 2 + 'px'
        this.dom.style.left = this.x - this.r + 'px'
        this.dom.style.top = this.y - this.r + 'px'
        this.dom.style.backgroundColor = this.color
        // 上树
        document.body.appendChild(this.dom)
      }
      // 更新
      Ball.prototype.update = function () {
        // 位置改变
        this.x += this.dX
        this.y -= this.dY
        // 半径改变
        this.r += 0.2
        // 透明度改变
        this.opacity -= 0.01
        this.dom.style.width = this.r * 2 + 'px'
        this.dom.style.height = this.r * 2 + 'px'
        this.dom.style.left = this.x - this.r + 'px'
        this.dom.style.top = this.y - this.r + 'px'
        this.dom.style.opacity = this.opacity

        // 当透明度小于0的时候,就需要从数组中删除自己,DOM元素也要删掉自己
        if (this.opacity < 0) {
          // 从数组中删除自己
          for (var i = 0; i < ballArr.length; i++) {
            if (ballArr[i] == this) {
              ballArr.splice(i, 1)
            }
          }
          // 还要删除自己的dom
          document.body.removeChild(this.dom)
        }
      }

      // 把所有的小球实例都放到一个数组中
      var ballArr = []

      // 初始颜色数组
      var colorArr = [
        '#66CCCC',
        '#CCFF66',
        '#FF99CC',
        '#FF6666',
        '#CC3399',
        '#FF6600',
      ]

      // 定时器,负责更新所有的小球实例
      setInterval(function () {
        // 遍历数组,调用调用的update方法
        for (var i = 0; i < ballArr.length; i++) {
          ballArr[i].update()
        }
      }, 20)

      // 鼠标指针的监听
      document.onmousemove = function (e) {
        // 得到鼠标指针的位置
        var x = e.clientX
        var y = e.clientY

        new Ball(x, y)
      }
    </script>
  </body>
</html>

:::

内置对象

包装类

  • 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 二进制浮点数算术标准,这会使一些个别的小数运算产生“丢失精度”问题

  • 解决办法:在进行小数运算时,要调用数字的 toFixed() 方法保留指定的小数位数

console.log((0.1 + 0.2).toFixed(1)) // 0.3

四舍五入 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)) + 1

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(时间戳)的写法,可以将时间戳变为日期对象
上次编辑于: