小讴

小讴 查看完整档案

北京编辑北京印刷学院  |  广告学 编辑中电智绘系统技术  |  前端高级工程师 编辑 github.com/simon9124 编辑
编辑

从前做策划、半路出家的前端程序猿,爱吉他爱户外爱篮球更爱写代码,努力为了更好的生活。大家可添加我微信(powerful_simon),互相交流互相学习,一起弹琴唱歌打球爬山。同时还望前辈们多多指点,多多帮忙内推~

个人动态

小讴 发布了文章 · 3月1日

《javascript高级程序设计》学习笔记 | 4.1.原始值与引用值

关注前端小讴,阅读更多原创技术文章

原始值与引用值

  • JS 变量是松散类型的:① 不必规定变量的数据类型 ② 变量的值和数据类型可随时改变
  • JS 变量可以包含 2 种类型的数据:原始值引用值

    • 原始值是简单数据(6 种原始值:Undefined、Null、Boolean、Number、String、Symbol),按值访问,操作实际值
    • 引用值是保存在内存中的对象,按引用访问,操作对该对象的引用(而非对象本身)

相关代码 →

动态属性

  • 对于引用值,可以随时添加、修改、删除其属性和方法
let personRefer = new Object() // 创建对象
personRefer.name = 'Nicholas' // 添加属性并赋值
console.log(personRefer.name) // 'Nicholas'
  • 对于原始值,不能拥有属性(尝试添加属性不会报错)
let namePrim = 'Nicholas' // 原始值
namePrim.age = 27 // 给原始值添加属性
console.log(namePrim.age) // undefined,不报错但无效
  • 用 new 关键字创建原始值包装类型对象,属于(类似于原始类型的)特殊引用类型
let name1 = 'Nicholas' // 原始类型
let name2 = new String('Matt') // 原始值包装类型
name1.age = 27
name2.age = 26
console.log(name1.age) // undefined,原始类型不能有属性
console.log(name2.age) // 26,引用类型可以有属性
console.log(typeof name1) // string,原始类型
console.log(typeof name2) // object,引用类型

复制值

  • 原始值复制时,新值是旧值的副本,新值和旧值相互独立使用、互不干扰
let num1Prim = 5
let num2Prim = num1Prim
console.log(num2Prim) // 5,原始值,复制的值是副本
num2Prim = 6 // 副本发生改变
console.log(num2Prim) // 6
console.log(num1Prim) // 5,被复制的值无变化
  • 引用值复制时,新值和旧值共同指向堆内存中的同一个对象,新值或旧值发生改变相互影响
let obj1Refer = new Object()
let obj2Refer = obj1Refer // 引用值,复制的值是指针
obj2Refer.name = 'Nicholas' // 一个对象发生改变
console.log(obj2Refer.name) // 'Nicholas'
console.log(obj1Refer.name) // 'Nicholas',影响另一个对象
delete obj1Refer.name // 一个对象发生改变
console.log(obj1Refer.name) // undefined
console.log(obj2Refer.name) // undefined,影响另一个对象

传递参数

  • ECMAScript 中所有函数的参数都是按值传递的——函数外的值被复制到函数内部的参数时,和一个变量复制到另一个变量一样:

    • 原始值作为参数时,函数内部改变参数,函数外的值不受影响
    • 引用值作为参数时,函数内部改变对象的属性,函数外的对象受影响
/* 原始值作为参数 */
let count = 10 // 函数外,原始值作为参数
function addTen(num) {
  num += 10 // 函数内,参数的值发生改变
  return num
}
let result = addTen(count)
console.log(result) // 30
console.log(count) // 20,未受影响

/* 引用值作为参数 */
let person = new Object() // 函数外,引用值作为参数
function setName(obj) {
  obj.name = 'Nicholas' // 函数内,obj和外部参数person指向同一个对象,并改变了这个对象的属性
}
setName(person)
console.log(person.name) // 'Nicholas',受影响
  • 尽管用引用值作为参数时,函数内部改变对象的属性会影响函数外的对象,但参数仍然是按值传递的,而不是按引用传递

    • 引用值作为参数时,如果在函数内部重写对象,函数外的对象不受影响,原始的引用仍然没变
    • 如果是按引用传递,重写函数内的对象参数后,原始引用应当发生改变
let person2 = new Object()
function setName2(obj) {
  obj.name = 'Nicholas' // 改变参数的属性,参数受影响
  obj = new Object() // 重写参数,参数不受该影响
  obj.name = 'Greg'
}
setName2(person2)
console.log(person2.name) // 'Nicholas'

确定类型

let str = 'Nicholas'
let num = 30
let boo = true
let u
let n = null
let sym = Symbol()
let f = new Function()
let o = new Object()
let a = new Array()
let r = new RegExp()
  • typeof 操作符可检测的类型:String、Number、Boolean、Symbol、Undefined、Function(不包含 Null 的所有原始值和 Function)
console.log(typeof str) // string,原始值
console.log(typeof num) // number,原始值,原始值
console.log(typeof boo) // boolean,原始值
console.log(typeof u) // undefined,原始值
console.log(typeof sym) // symbol,原始值
console.log(typeof f) // function,引用值但是Function会返回function
  • typeof 操作符检测任何引用值或 null,结果都是 object
console.log(typeof n) // object,原始值但是Null会返回object
console.log(typeof o) // object,除Function之外的引用值都返回object
console.log(typeof a) // object,除Function之外的引用值都返回object
console.log(typeof r) // object,除Function之外的引用值都返回object
  • instanceof 操作符可检测的类型:Object(所有引用值)
console.log(o instanceof Object) // true,o是Object的实例
console.log(f instanceof Function) // true,f是Function的实例
console.log(f instanceof Array) // false,f不是Array的实例
console.log(a instanceof Array) // true,a是Array的实例
console.log(r instanceof RegExp) // true,r是RegExp的实例
  • 所有引用值都是 Object 的实例
console.log(f instanceof Object) // true,所有引用类型都是Object的实例
console.log(a instanceof Object) // true,所有引用类型都是Object的实例
console.log(r instanceof Object) // true,所有引用类型都是Object的实例
  • instanceof 操作符检测任何原始值,结果都是 false
console.log(n instanceof Object) // false,原始值不是对象,不是Object的实例

总结 & 问点

原始值引用值
简单数据保存在内存中的对象
按值访问按引用访问
操作实际值操作对该对象的引用(而非对象本身)
不能拥有属性随时增、删、改其属性和方法
复制的值是副本,相互不影响复制的值是指针,相互影响
作为参数时,函数内部改变值不影响参数作为参数时,函数内部改变属性会影响参数,重写不影响
typeof 判断类型(null 返回 object)instanceof 判断类型
  • 如何理解 JS 的变量是松散类型的?
  • 在访问方式和操作方式上,原始值和引用值有什么不同?
  • 在通过变量复制时,原始值和引用值有什么不同?
  • 作为函数的参数传递时,原始值和引用值有什么不同?
  • 如何“按值传递函数的参数”?为什么引用值作为函数的参数也是按值传递而不是按引用传递的?
  • 如何判断一个原始值或引用值的具体类型?
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2月3日

《javascript高级程序设计》学习笔记 | 3.3.变量

关注前端小讴,阅读更多原创技术文章

变量

  • ECMAScript 变量是松散类型的:变量可以保存任何类型的数据
  • 3 个声明变量的关键字:var、const、let

相关代码 →

var 关键字

  • 不初始化时,变量保存 undefined
var message
console.log(message) // undefined
  • 初始化变量只是设置变量的值,可以改变保存的值,也可以改变值的类型
var message = 'hi'
message = 100 // 合法,但不推荐
console.log(message) // 100

var 声明作用域

  • 使用 var 操作符定义的变量,会成为包含它的函数局部变量
  • 函数退出(调用)时,该变量被销毁
function test() {
  var messageTest = 'hi' // 在函数内部创建变量并赋值
}
test() // 调用函数,内部变量被销毁
console.log(messageTest) // ReferenceError: messageTest is not defined
  • 函数内定义变量时省略 var 操作符,可以创建一个全局变量
function test() {
  messageTest = 'hi' // 省略var操作符,全局变量
}
test() // 调用函数,定义内部的全局变量
console.log(messageTest) // 'hi
  • 不推荐在函数内部通过省略 var 关键字定义全局变量:

    • 局部作用域中定义的全局变量很难维护
    • 会造成困惑,无法断定省略 var 是有意为之还是语法错误
    • 严格模式下,抛出 ReferenceError

var 声明提升

  • 使用 var 声明的变量,(无论实际声明的位置在何处)会自动提升到函数作用域顶部;如果声明不在函数内,则被提升到全局作用域顶部
function foo() {
  console.log(age)
  var age = 30
}
foo() // undefined,不报错,变量声明提升到函数作用域顶部
console.log(age)
var age = 30 // undefined,不报错,变量声明提升到全局作用域顶部
  • 以上代码等价于:
var age
function foo() {
  var age
  console.log(age)
  age = 30
}
foo() // undefined
console.log(age) // undefined
age = 30
  • 可反复多次使用 var 声明同一个变量
function fooAge() {
  var age = 16
  var age = 26
  var age = 36
  console.log(age)
}
fooAge() // 36,可反复多次使用var声明同一个变量

let 声明

  • 与 var 作用差不多,但声明范围是块作用域(var 是函数作用域)
if (true) {
  var nameVar = 'Matt'
  console.log(nameVar) // 'Matt'
}
console.log(nameVar) // 'Matt'
if (true) {
  let nameLet = 'Matt' // 作用域仅限块内部
  console.log(nameLet) // 'Matt'
}
console.log(nameLet) // ReferenceError: nameLet is not defined
  • let 出现过的同一个块作用域中, 不允许出现冗余声明
var nameVar
var nameVar // var允许重复声明同一个变量

let nameLet
let nameLet // Identifier 'nameLet' has already been declared,冗余声明

let nameVar // Identifier 'nameVar' has already been declared
var nameLet // Identifier 'nameLet' has already been declared
  • 若同一个块中没有重复声明,可嵌套使用相同的标识符
let ageNest = 30
console.log(ageNest) // 30
if (true) {
  let ageNest = 28
  console.log(ageNest) // 28,在不同的块中
}

暂时性死区

  • let 声明的变量不会在作用域中被提升(var 可以提升)
  • 在 let 声明之前的执行瞬间被称为“暂时性死区”,此阶段引用任何候面才声明的变量都会抛出 ReferenceError
console.log(ageVarPromote) // undefined
var ageVarPromote = 26
console.log(ageLetPromote) // ReferenceError: ageLetPromote is not defined
let ageLetPromote = 26

全局声明

  • 在全局作用域中,let 声明的变量不会成为 window 对象的属性(var 声明的变量则会)
var nameVarWhole = 'Matt'
console.log(window.nameVarWhole) // 'Matt0',vscode没有window对象,在浏览器印证
let nameLetWhole = 'Matt'
console.log(window.nameLetWhole) // undefined,vscode没有window对象,在浏览器中印证

条件声明

  • 对于 let 声明,不能依赖条件声明模式(条件声明是反模式,如果发现正在使用这个模式,则定有更好的替代方式)
// typeof操作符
if (typeof nameLetCondition === 'undefined') {
  let nameLetCondition = 'Matt' // 仅在块级作用域内
  console.log(nameLetCondition) // 'Matt'
}
console.log(nameLetCondition) // ReferenceError: nameLetCondition is not defined

// try/catch语句
try {
  console.log(nameLetCondition2)
} catch (error) {
  let nameLetCondition2 = 'Matt' // 仅在块级作用域内
  console.log(nameLetCondition2) // 'Matt'
}
console.log(nameLetCondition2) // ReferenceError: nameLetCondition2 is not defined

for 循环中的 let 声明

  • let 声明迭代变量的作用域同样仅限于 for 循环块内部(var 会渗透到循环体外部)
for (var iVar = 0; iVar < 5; iVar++) {}
console.log(iVar) // 5,循环体外部受影响
for (let iLet = 0; iLet < 5; iLet++) {}
// console.log(iLet) // ReferenceError: iLet is not defined
  • let 声明迭代变量,JS 引擎会为每个迭代循环声明新的迭代变量(var 保存的是导致循环退出的值)
// 使用var声明:退出循环时,迭代变量保存的是导致循环退出的值
for (var iVarDelay = 0; iVarDelay < 5; iVarDelay++) {
  // 超时逻辑在退出循环后执行,此时变量的值为5
  setTimeout(() => {
    console.log(iVarDelay) // 5、5、5、5、5
  }, 0)
}
// 使用let声明:为每个迭代循环声明新的迭代变量
for (let iLetDelay = 0; iLetDelay < 5; iLetDelay++) {
  // 超时逻辑在退出循环后执行,变量值分别为每个新的迭代变量
  setTimeout(() => {
    console.log(iLetDelay) // 0、1、2、3、4
  }, 0)
}

const 声明

  • 与 let 行为基本相同,但声明时必须赋初始值
const ageConst // SyntaxError: Missing initializer in const declaration
  • 尝试修改 const 定义的变量,会报错(或者说定义的是常量)
const ageConst = 26
ageConst = 28 // TypeError: Assignment to constant variable.
  • const 也不允许重复声明
const ageConst = 26
const ageConst = 28 // SyntaxError: Identifier 'ageConst' has already been declared
  • const 声明的作用域也是块
if (true) {
  const nameConst = 'Nicholas'
}
console.log(nameConst) // ReferenceError: nameConst is not defined
  • const 声明的限制只适用于指向的内存地址不得改动(指针):

    • 对于基本类型来说,不得修改值
    • 对于引用类型的对象来说,不得重写地址,但可以修改其内部属性
const person = {}
console.log(person.name) // undefined
person.name = 'Matt'
console.log(person.name) // 'Matt',未重写地址,仅修改对象的内部属性
person = { name: 'Matt' } // TypeError: Assignment to constant variable,重写地址
  • 不能用 const 声明迭代变量,因为迭代变量会自增
for (const index = 0; index < 5; index++) {} // TypeError: Assignment to constant variable.
  • 可以用 const 声明不会被修改的 for 循环变量,这对于 for-in 和 for-of 循环特别有意义
let i = 0
for (const j = 7; i < 5; i++) {
  console.log(j) // 7、7、7、7、7
}
for (const key in { a: 1, b: 2 }) {
  console.log(key) // a、b
}
for (const value of 'Matt') {
  console.log(value) // 'M'、'a'、't'、't'
}

声明风格及最佳实践

  • 尽量不使用 var,只使用 let 和 const

    • 变量有了明确的作用域声明位置不变的值
  • 优先使用 const,let 次之

    • 让浏览器运行时强制保持变量不变,让静态代码分析工具提前发现不合法的赋值操作
    • 提前知道会有修改再使用 let

总结 & 问点

操作符重写值作用域声明提升在全局作用域中声明声明冗余条件声明模式for 循环中最佳实践
var可以函数作用域成为 window 对象的属性允许影响全局属性迭代变量保存的是导致循环退出的值尽量不用
let可以块作用域不成为 window 对象的属性不允许只影响块中属性每个迭代循环声明一个新的迭代变量次之
const不可,可修改对象内部属性块作用域不成为 window 对象的属性不允许只影响块中属性只能声明不会被修改的 for 循环变量首选
  • 如何理解“JS 的变量是松散类型”的?JS 变量初始化后,可以改变值的类型么?
  • 在函数中,用 var 操作符定义的变量,在调用函数后会怎样?若省略 var 操作符呢?
  • 变量提升的含义是什么?为什么不推荐在函数中用省略 var 操作符的办法定义全局变量?
  • let 声明的范围是什么?其在全局声明、条件声明和迭代循环时,有哪些特点?
  • 详细说明 let 声明和 var 声明有哪些异同?
  • 可以修改用 const 声明的数组或对象的值或属性吗?为什么?
  • 详细说明 let 声明和 const 声明有哪些异同?
  • 声明变量时,有哪些助于提升生代码质量的最佳实践?
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 1月4日

《javascript高级程序设计》学习笔记 | 6.2.创建对象

关注前端小讴,阅读更多原创技术文章

创建对象

  • 创建单个对象:Object 构造函数 和 对象字面量
  • 缺点:使用一个接口创建很多对象,产生大量重复代码

相关代码 →

工厂模式

  • 抽象了创建具体对象的过程
  • 用函数来封装以特定接口创建对象的细节
function createPerson(name, age, job) {
  var o = new Object()
  o.name = name
  o.age = age
  o.job = job
  o.sayName = function () {
    console.log(this.name)
  }
  return o
}
var person1 = createPerson('Nicholas', 29, 'Engineer')
var person2 = createPerson('Greg', 27, 'Doctor')
console.log(person1)
console.log(person2)
  • 工厂模式解决了创建多个相似对象的问题,但没有解决对象识别问题——即怎样知道一个对象的类型

构造函数模式

  • 除了 Object 和 Array 等原生构造函数,还可以创建自定义的构造函数
function Person(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = function () {
    console.log(this.name)
  }
}
var person1 = Person('Nicholas', 29, 'Software Engineer')
var person2 = Person('Greg', 27, 'Doctor')
  • 构造函数模式 vs 工厂模式:① 不显式的创建对象;② 直接将属性和方法赋给 this 对象;③ 没有 return
  • 构造函数 new 一个对象后:① 创建了一个新对象;② 将构造函数的作用域(即 this)赋给新对象;③ 执行构造函数中的代码(即:为这个对象添加新属性);④ 返回新对象
  • 构造函数用大写字母开头,创建实例时用 new 操作符
  • 创建的对象的 constructor 属性指向构造函数
  • 创建的对象既是 Object 的实例,又是构造函数的实例
console.log(person1.constructor === Person) // true,constructor 属性指向构造函数
console.log(person2.constructor === Person) // true,constructor 属性指向构造函数
console.log(person1 instanceof Object) // true,是 Object 的实例
console.log(person1 instanceof Person) // true,也是 Person 的实例
console.log(person2 instanceof Object) // true,是 Object 的实例
console.log(person2 instanceof Person) // true,也是 Person 的实例
  • 可以将自定义构造函数的实例标识为一种特定的类型,这是构造函数模式胜过工厂模式的地方
  • 以该方法定义的构造函数是定义在 Global 对象中的,在浏览器中则是 window 对象
// 构造函数vs普通函数
var person3 = new Person('Nicholas', 29, 'Software Engineer') // 用构造函数创建对象
person3.sayName() // 'Nicholas'
Person('Greg', 27, 'Doctor') // 不使用new操作符,直接调用
global.sayName() // 直接调用函数时,this指向Global对象(浏览器中指向window对象)
var o = new Object() // 新对象o
var p = new Object() // 新对象p
Person.call(o, 'Kristen', 25, 'Nurse') // 扩充作用域,在对象o中调用Person()函数,call()分别传入每个参数
Person.apply(p, ['Kristen', 25, 'Nurse']) // 扩充作用域,在对象p中调用Person()函数,apply()传入参数数组
o.sayName() // 'Kristen'
p.sayName() // 'Kristen'
  • 构造函数的问题在于,对象的每个方法都要在每个实例上重新创建一遍,既“每定义一个函数,就实例化一个对象”
  • 而创建 2 个完成同样任务的 Function 实例没有必要
function Person2(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = new Function(console.log(this.name)) // 与声明函数逻辑等价,每创建一个对象就要创建一个Function实例
}
console.log(person1.sayName === person2.sayName) // false,新对象的2个方法的作用域链和标识符解析不同
  • 将对象的方法移到构造函数外部,避免多次创建 Function 实例
function Person3(name, age, job) {
  this.name = name
  this.age = age
  this.job = job
  this.sayName = sayName
}
function sayName() {
  console.log(this.name) // 将sayName设置成全局函数
}
var person4 = new Person('Nicholas', 29, 'Software Engineer')
  • 构造函数仍未解决的问题:① 创建的全局函数实际上只需要被某个对象中调用;② 若对象有多个方法,则需创建很多全局对象

原型模式

  • 每个函数都有 prototype 原型属性,该属性是一个指针,指向函数的原型对象,并包含特定类型的所有实例共享的属性和方法,即“通过调用构造函数-而创建的那个对象实例的-原型对象”
  • 使用原型对象的好处是,其所有对象实例共享其所包含的属性和方法

理解原型对象

function PersonPrototype() {}
PersonPrototype.prototype.name = 'Nicholas' // 为PersonPrototype的原型对象添加属性
PersonPrototype.prototype.age = 29 // 为PersonPrototype的原型对象添加属性
PersonPrototype.prototype.job = 'Software Engineer' // 为PersonPrototype的原型对象添加属性
PersonPrototype.prototype.sayName = function () {
  // 为PersonPrototype的原型对象添加方法
  console.log(this.name)
}
var person5 = new PersonPrototype()
var person6 = new PersonPrototype()
person5.sayName() // 'Nicholas'
person6.sayName() // 'Nicholas'
console.log(person5.sayName === person6.sayName) // true,prototype上创建的属性和方法,由新对象的所有实例共享
  • 原型对象自动获得 constructor(构造函数)属性,指向 prototype 属性所在函数的指针,即构造函数
console.log(PersonPrototype.prototype.constructor) // Function: PersonPrototype构造函数
console.log(PersonPrototype === PersonPrototype.prototype.constructor) // true,都指向构造函数
  • 实例内部包含[[Prototype]]指针,指向实例的构造函数的原型对象,但没有标准的方式访问[[Prototype]]
  • 在浏览器中,可用 __proto__ 属性实现[[Prototype]]的功能
console.log(person5.__proto__) // 原型对象,PersonPrototype {name: 'Nicholas',age: 29,job: 'Software Engineer',sayName: [Function] }
console.log(person5.__proto__ === PersonPrototype.prototype) // true,都指向原型对象
console.log(person5.__proto__.constructor) // Function: PersonPrototype构造函数
  • 原型对象的 isPrototypeOf()方法,检测实例中否有指向原型对象的指针
console.log(PersonPrototype.prototype.isPrototypeOf(person5)) // true,person5包含指向PersonPrototype的原型对象的指针
console.log(PersonPrototype.prototype.isPrototypeOf(person1)) // false,person1不包含指向PersonPrototype的原型对象的指针
  • ES5 追加 Object.getPrototypeOf()方法,参数为实例,返回实例的构造函数的原型对象
console.log(Object.getPrototypeOf(person5)) // 原型对象
console.log(Object.getPrototypeOf(person5) === person5.__proto__) // true,都指向原型对象
console.log(Object.getPrototypeOf(person5) === PersonPrototype.prototype) // true,都指向原型对象
console.log(Object.getPrototypeOf(person5).name) // 'Nicholas'
console.log(Object.getPrototypeOf(person5).constructor) // Function: PersonPrototype构造函数
  • 代码读取对象属性的搜索过程:

    • 1.搜索对象实例本身 -> 有属性 → 返回属性值 -> 结束
    • 2.对象实例本身无属性 -> 搜索原型对象 → 有/无属性 → 返回属性值/undefined → 结束
  • 可以通过实例访问原型中属性的值,但无法通过实例重写原型中属性的值
  • 如果添加的实例属性与原型的属性同名,则实例属性屏蔽原型中的属性
  • 删除同名的实例属性,可恢复被屏蔽的原型的属性
var person7 = new PersonPrototype()
person7.name = 'Greg'
console.log(person7.name) // 'Greg',来自实例
console.log(person5.name) // 'Nicholas',来自原型
delete person7.name
console.log(person7.name) // 'Nicholas',来自原型
  • 使用 hasOwnProperty('属性') 检测属性存在于实例 or 原型,存在于实例返回 true
var person8 = new PersonPrototype()
var person9 = new PersonPrototype()
console.log(person8.hasOwnProperty('name')) // false,name不存在在person8的实例中
person8.name = 'Simon'
console.log(person8.name) // 'Simon',来自实例
console.log(person8.hasOwnProperty('name')) // true,name存在在person8的实例中
console.log(person9.name) // 'Nicholas',来自原型
console.log(person9.hasOwnProperty('name')) // false,name不存在在person8的实例中
delete person8.name
console.log(person8.name) // 'Nicholas',来自原型
console.log(person8.hasOwnProperty('name')) // false,person8实例的name属性已被删除
  • 可在原型对象上调用 Object.getOwnPropertyDescriptor(),获取原型属性的描述符
console.log(Object.getOwnPropertyDescriptor(person8, 'name')) // undefined,person8实例上没有name属性
console.log(Object.getOwnPropertyDescriptor(person8.__proto__, 'name')) // {value: 'Nicholas',writable: true,enumerable: true,configurable: true},原型对象的name属性描述符

原型与 in 操作符

  • 单独使用 in:对象能够访问指定属性则返回 true,无论属性在实例中还是原型中
function PersonIn() {}
PersonIn.prototype.name = 'Nicholas'
PersonIn.prototype.age = 29
PersonIn.prototype.job = 'Software Engineer'
PersonIn.prototype.sayName = function () {
  console.log(this.name)
}
var person9 = new PersonIn()
var person10 = new PersonIn()
console.log(person9.hasOwnProperty('name')) // false,实例person9中不含name属性
console.log('name' in person9) // true,通过person9可以访问到name属性
person9.name = 'Greg'
console.log(person9.name); // 'Greg',来自实例
console.log(person9.hasOwnProperty('name')) // true,实例person9中包含name属性
console.log('name' in person9) // true,通过person9可以访问到name属性
console.log(person10.name); // 'Nicholas',来自原型
console.log(person10.hasOwnProperty('name')) // false,实例person10中不含name属性
console.log('name' in person10) // true,通过person10可以访问到name属性
delete person9 'name'
console.log(person9.name); // 'Nicholas',来自原型
console.log(person9.hasOwnProperty('name')) // false,实例person9中不含name属性
console.log('name' in person9) // true,通过person9可以访问到name属性
  • 同时使用 hasOwnProperty 和 in,判断属性存在于 对象 or 原型
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && name in object
}
var person11 = new PersonIn()
console.log(hasPrototypeProperty(person11, 'name')) // true,!false && true
person11.name = 'Greg'
console.log(hasPrototypeProperty(person11, 'name')) // false,!true && true
  • for-in 循环使用 in:返回所有能够通过对象访问的、可枚举的属性(无论来自实例还是原型),屏蔽了原型中不可枚举的属性([[Enumerable]]为 false 的属性,如 constructor 和 prototype)
for (var attr in person11) {
  console.log(`${attr}:${person11[attr]}`)
  /*  
    name:Greg
    age:29
    job:Software Engineer
    sayName:function () {
      console.log(this.name)
    } 
  */
}
  • Object.keys():接收一个对象作为参数,返回该对象上(仅该对象自身)可枚举的属性的数组
var keys = Object.keys(PersonIn.prototype) // 原型对象的所有可枚举属性
console.log(keys) // [ 'name', 'age', 'job', 'sayName' ]
var person12 = new PersonIn()
person12.name = 'Bob'
person12.age = 31
var p12keys = Object.keys(person12) // person12的所有可枚举属性
console.log(p12keys) // [ 'name', 'age' ]
  • Object.getOwnPropertyNames():获取该对象上(仅该对象自身)所有属性的数组,无论是否可枚举
var keys = Object.getOwnPropertyNames(PersonIn.prototype) // 原型对象的所有属性,包含不可枚举
console.log(keys) // [ 'constructor', 'name', 'age', 'job', 'sayName' ],原型对象都包含constructor属性,指向构造函数
var p12keys = Object.getOwnPropertyNames(person12) // person12的所有属性,包含不可枚举
console.log(p12keys) // [ 'name', 'age' ]

更简单的原型语法

  • 包含所有属性和方法新对象字面量来重写整个原型对象
function PersonLiteral() {}
PersonLiteral.prototype = {
  name: 'Nicholas',
  age: 29,
  job: 'Software Engineer',
  sayName: function () {
    console.log(this.name)
  },
}
  • 将构造函数的 prototype 属性设置为一个以对象字面量形式创建新对象,其 constructor 属性不再指向原构造函数,而是
    新对象的 constructor 属性,即 Object 构造函数
var friend = new PersonLiteral()
console.log(friend instanceof Object) // true,friend是Object的实例
console.log(friend instanceof PersonLiteral) // true,friend是PersonLiteral的实例
console.log(friend.constructor === PersonLiteral) // false,constructor属性变成了新对象——即对象字面量的constructor
console.log(friend.constructor === Object) // true,新对象的constructor指向Object
  • 可以在对象字面量里设置 constructor 属性,让其指向原构造函数
  • 这样设置 constructor 属性属于“直接在对象上定义的属性”,会导致 PersonLiteral2.prototype 的 constructor 属性的[[Enumerable]]为 true,可以被循环返回
function PersonLiteral2() {}
PersonLiteral2.prototype = {
  constructor: PersonLiteral2, // 直接在对象上定义constructor,指向原构造函数
  name: 'Nicholas',
  age: 29,
  job: 'Software Engineer',
  sayName: function () {
    console.log(this.name)
  },
}
var friend2 = new PersonLiteral2()
console.log(friend2.constructor === PersonLiteral2) // true,constructor再次指向原构造函数
console.log(friend2.constructor === Object) // false
var keys = Object.keys(PersonLiteral2.prototype)
console.log(keys) // [ 'constructor', 'name', 'age', 'job', 'sayName' ],因为constructor是“直接在对象上定义的属性”
  • 用 Object.defineProperty()修改对象字面量中 constructor 属性的特性,以兼容 ES5 的 javascript 引擎
Object.defineProperty(PersonLiteral2.prototype, 'constructor', {
  enumerable: false,
  value: PersonLiteral2,
})
var keys = Object.keys(PersonLiteral2.prototype)
console.log(keys) // [ 'name', 'age', 'job', 'sayName' ],constructor的enumerable已被设置为false

原型的动态性

  • 对原型对象所做的任何修改都立即从实例上反映出来,即使先创建实例后修改原型
function Person4() {}
var friend3 = new Person4()
Person4.prototype.sayHi = function () {
  console.log('Hi')
}
friend3.sayHi() // 'Hi',先在friend3实例中搜索sayHi属性,没有找到则继续找原型对象
  • 重写整个原型对象,会切断构造函数与最初原型之间的联系,而实例的[[Prototype]]指针指向最初的原型对象
Person4.prototype = {
  constructor: Person4,
  name: 'Nicholas',
  age: 29,
  job: 'Software Engineer',
  sayName: function () {
    console.log(this.name)
  },
}
console.log(friend3.__proto__) // Person4 { sayHi: [Function] },最初的原型对象
console.log(friend3.__proto__ === Person4.prototype) // false,实例的__proto__指向最初的原型对象,重写整个原型切断了构造函数与最初原型之间的联系
friend3.sayName() // error:friend3.sayName is not a function

原生对象的原型

  • 所有原生的引用类型(Array、Object、String...),都是用原型模式创建的,在其构造函数的原型上定义了方法
console.log(Array.prototype) // 在浏览器中查看Array的原型对象,包含sort()等方法
console.log(String.prototype) // 在浏览器中查看Array的原型对象,包含substring()等方法
  • 可以像修改自定义对象的原型一样,修改原生对象的原型,添加或删除方法
  • 不推荐修改原生对象的原型,可能会引起冲突或重写原生方法
String.prototype.startsWith = function (text) {
  // 给String的原型对象添加startsWith方法
  return this.indexOf(text) === 0
}
var msg = 'Hello World'
console.log(msg.startsWith('Hello')) // true
console.log(msg.startsWith('World')) // false
delete String.prototype.startsWith
console.log(msg.startsWith('Hello')) // error

原型对象的问题

  • 原型模式最大的问题是由其共享的本性导致的,尤其对于包含引用类型的属性,对实例的数组、对象等引用类型的属性进行增删改而非重新定义时,会对原型的引用类型属性造成影响
function PersonProblem() {}
PersonProblem.prototype = {
  constructor: PersonProblem,
  name: 'Nicholas',
  age: 29,
  job: 'Software Engineer',
  friends: ['Shelby', 'Court'],
  sayName: function () {
    console.log(this.name)
  },
}
var person13 = new PersonProblem()
var person14 = new PersonProblem()
person13.name = 'Greg' // 重新定义,在实例中屏蔽原型的属性
person13.friends.push('Van') // 非重新定义,而是向原型的数组中添加一个字符串
console.log(person13.name) // 'Greg',从实例获得
console.log(person14.name) // 'Nicholas',从原型中获得
console.log(person13.friends) // [ 'Shelby', 'Court', 'Van' ],从原型中获得
console.log(person14.friends) // [ 'Shelby', 'Court', 'Van' ],从原型中获得
console.log(person13.friends === person14.friends) // true
var person15 = new PersonProblem()
person15.friends = [] // 重新定义,在实例中屏蔽原型的属性
console.log(person15.friends) // [],从实例获得
console.log(person13.friends) // [ 'Shelby', 'Court', 'Van' ],从原型中获得
console.log(person14.friends) // [ 'Shelby', 'Court', 'Van' ],从原型中获得

组合使用构造函数模式和原型模式

  • 构造函数模式用于定义实例属性,原型模式用域定义方法共享的属性
  • 每个实例都有自己的一份实例属性的副本,同时共享着对方法的引用,最大限度节省内存
  • 该模式是目前 ECMAScript 中使用最广泛、认同度最高的创建自定义类型的方法
function PersonMix(name, age, job) {
  // 在构造函数定义实例属性
  this.name = name
  this.age = age
  this.job = job
  this.friends = ['Shelby', 'Court']
}
PersonMix.prototype = {
  // 在原型定义方法和共享的属性
  constructor: PersonMix, // 直接在对象上定义constructor属性,指向构造函数,[[Enumerable]]为true
  sayName: function () {
    console.log(this.name)
  },
}
var person16 = new PersonMix('Nicholas', 29, 'Software Engineer')
var person17 = new PersonMix('Greg', 27, 'Doctor')
person16.friends.push('Van') // 仅向person16实例本身的friends数组push数据
console.log(person16.friends) // [ 'Shelby', 'Court', 'Van' ]
console.log(person17.friends) // [ 'Shelby', 'Court' ]
console.log(person16.friends === person17.friends) // false,person16实例的friends数组push了数据
console.log(person16.sayName === person17.sayName) // true,共享方法sayName

动态原型模式

  • 将所有信息(属性、方法)都封装在构造函数中,通过检查某个方法是否有效,来决定是否需要初始化原型
function PersonDynamic(name, age, job) {
  // 属性
  this.name = name
  this.age = age
  this.job = job
  // 方法:①只有方法不存在时才添加到原型;②只在初次调用构造函数时执行;③会立即在实例中体现
  if (typeof this.sayName !== 'function') {
    PersonDynamic.prototype.sayName = function () {
      console.log(this.name)
    }
  }
}
var person18 = new PersonDynamic('Nicholas', 29, 'Software Engineer')
person18.sayName() // 'Nicholas'
console.log(person18 instanceof PersonDynamic) // true,person18是PersonDynamic的实例
  • 只有在方法不存在时,才会将方法添加到原型
  • 只在初次调用构造函数时才会执行,此后原型已经完成初始化
  • 对原型所做的修改,会立即在实例中体现
  • 原型的动态性同理,如果已经创建了实例后再用对象字面量重写原型,会切断实例与新原型之间的联系,导致修改无效
PersonDynamic.prototype = {
  newName:
    typeof this.newName !== 'function'
      ? function () {
          console.log('prototype:', this.name)
        }
      : this.newName,
}
person18.newName() // error,person18指向最初的原型,没有newName方法
var person19 = new PersonDynamic('Greg', 27, 'Doctor') // person19是重写原型后创建的实例
person19.newName() // prototype: Greg
person19.sayName() // Greg

console.log(person18 instanceof PersonDynamic) // false,person18不是重写原型后的PersonDynamic的实例,person18指向最初的原型
console.log(person19 instanceof PersonDynamic) // true,person19是重写原型后的PersonDynamic的实例

寄生构造函数模式

  • 构造函数仅封装创建对象的代码,在构造函数内部用原生引用类型创建新对象,经过操作后再返回这个新对象
  • 构造函数内部跟工厂模式一模一样,调用时用 new 操作符,类似于工厂模式与构造函数模式的结合体
function PersonParasitic(name, age, job) {
  var o = new Object() // 用原生引用类型创建对象
  o.name = name // 添加值
  o.age = age // 添加值
  o.job = job // 添加值
  // 添加方法
  o.sayName = function () {
    console.log(this.name)
  }
  return o
}
var person20 = new PersonParasitic('Nicholas', 29, 'Software Engineer')
person20.sayName() // 'Nicholas'
  • 寄生构造函数模式创建的实例,与构造函数或构造函数的原型没有关联,不能依赖 instanceof 确定对象类型
  • 如果可以用其他模式,不建议该模式创建自定义对象
function SpecialArray() {
  var values = new Array() // 用原生引用类型创建数组
  values.push.apply(values, arguments) // 添加值
  // 添加方法
  values.toPipedString = function () {
    return this.join('|')
  }
  return values // 返回经过操作后的数组
}
var colors = new SpecialArray('red', 'blue', 'green')
console.log(colors.toPipedString()) // red|blue|green
console.log(SpecialArray.prototype) // SpecialArray{},构造函数的原型对象
console.log(colors.__proto__) // [],构造函数内部通过new Array()重新初始化,其原型对象是原生对象Array
console.log(SpecialArray.prototype === colors.__proto__) // false,二者无关联
console.log(colors instanceof SpecialArray) // false,二者无关联

稳妥构造函数模式

  • 与计生构造函数类似类似,在构造函数中用原生引用类型创建新对象,但没有公共属性其方法也不引用 this不使用 new 调用构造函数
  • 除了构造函数内部的方法,没有其他办法可访问到构造函数内部的原始数据(即便有其他代码给对象添加方法或属性),该模式适合在安全环境下使用
  • 寄生构造函数模式,该模式下创建的实例,与构造函数或构造函数的原型没有关联,不能依赖 instanceof 确定对象类型
function PersonSafe(name, age, job) {
  var o = new Object() // 用原生引用类型创建对象
  o.sayName = function () {
    console.log(name)
  }
  return o
}
var person21 = new PersonParasitic('Nicholas', 29, 'Software Engineer')
person21.sayName() // 'Nicholas'

总结 & 问点

创建对象过程缺点
Object 构造函数1.创建 Objecty 实例 2.添加属性和方法同一接口创建多个对象,大量重复代码
对象字面量直接创建包含属性和方法的对象同一接口创建多个对象,大量重复代码
工厂模式1.用函数封装创建 Object 实例的过程(添加属性和方法、返回该实例对象) 2.调用该函数没有解决对象识别问题,即怎样知道一个对象的类型
构造函数模式1.构造函数封装(不显示的创建对象、属性和方法赋给 this 对象、无 return) 2.new 调用构造函数每个实例重新创建方法,机制相同的 Function 对象被多次实例化
原型模式1.构造函数封装(空的,无属性和方法) 2.原型对象上添加属性和方法 3.new 调用构造函数对实例的引用类型属性修改而非重新定义时,会对原型的引用类型属性造成影响
组合使用构造函数模式和原型模式1.构造函数封装实例属性 2.原型模式定义方法和共享属性 3.new 调用构造函数无明显缺点,目前最广泛认同度最高,是默认模式
动态原型模式1.构造函数封装所有信息(属性、检查某个方法是否有效来决定是否初始化原型) 2.new 调用构造函数无明显缺点,不要使用对象字面量重写原型
寄生构造函数模式1.构造函数封装(原生引用类型创建新对象、添加值或方法、返回新对象) 2.new 调用构造函数实例与构造函数或构造函数的原型没有关联,不能依赖 instanceof 确定对象类型
稳妥构造函数模式1.同寄生构造函数模式的构造函数封装(但方法不引用 this) 2.作为普通函数调用构造函数实例与构造函数或构造函数的原型没有关联,不能依赖 instanceof 确定对象类型
对象属性指向用法
任何函数prototype原型对象Person.prototype → 构造函数的原型对象
实例、原型constructor构造函数person1.constructor === Person.prototype.constructor === Person
实例[[Prototype]]原型对象person1.__proto__ === Person.prototype(没有标准方式访问[[Prototype]],但可用 __proto__
操作符含义用法
new创建构造函数的实例(四个步骤var person = new Person()
in能否通过对象访问到属性(无论属性在实例还是原型中)console.log('name' in person)
for-in返回所有能通过对象访问到的、可枚举的属性(无论属性在实例还是原型中)for(var attr in person){console.log(attr)}
delete删除实例属性delete person.name
对象方法含义参数返回值用法
Object 对象getPrototypeOf()获取实例对象的原型实例原型对象Object.getPrototypeOf('person') === Person.prototype
任何对象hasOwnProperty()对象自身(不包括原型)是否含有该属性属性true/falseconsole.log(Person.hasOwnProperty('name'))
Object 对象keys()获取对象上所有可枚举的属性(仅对象自身)对象属性的字符串数组Object.keys(person)
Object 对象getOwnPropertyNames()获取对象上所有属性(仅对象自身,无论是否可枚举)对象属性的字符串数组Object.getOwnPropertyNames(person),原型对象会包含 constructor 属性
  • 创建单个对象有哪些方法?这些方法有什
    么缺点?
  • 工厂模式做出了怎样的优化?该模式有什么缺点?
  • 相比工厂模式,构造函数模式有哪些区别?如何创建其实例?
  • 构造函数在 new 的过程中都发生了什么?
  • 构造函数创建出的对象,其 construtor 属性指向哪里?这样的对象是哪些构造函数的实例?
  • 相比工厂模式,构造函数有什么优势?
  • 构造函数与普通函数有什么相同点和区别?
  • 自定义对象的方法时,构造函数模式有什么缺点?
  • 用全局函数代替构造函数内部的对象属性的方法,仍有什么缺点?
  • prototype 属性的含义和用法?使用原型对象的好处是什么?
  • 原型对象的 constructor 属性的含义和用法?
  • 用什么方法检测实例是否含有指向原型对象的指针?
  • 构造函数、实例的哪些属性或方法可获得原型对象?如何通过实例获得构造函数?
  • 代码读取对象属性时,经历了怎样的搜索过程?
  • 是否可以通过实例访问和修改原型中的属性值?
  • 在实例中添加与原型的同名属性会怎样?再删除这个实例中的属性呢?
  • 用什么方法检测属性存在与实例 or 原型?
  • 用什么方法获取原型属性的描述符?
  • 单独使用 in 的用法是什么?其和 hasOwnProperty()方法的区别是什么?
  • for-in 的用法是什么?其返回哪些属性屏蔽哪些属性?
  • Object.keys()的和 Object.getOwnPropertyNames()用法分别是什么?
  • Object.getOwnPropertyNames()、Object.leys()、for-in 的区别是什么?
  • 用一个对象字面量的新对象重写整个原型对象时,原型对象的 constructor 指向发生了怎样的改变?
  • 写一段代码,用对象字面量重写构造函数的原型对象,且让原型对象的 constructor 仍指向原构造函数,并保留 construtor 的[[Enumerable]]特性为 false
  • 创建实例后再修改原型的属性,实例会受到影响么?为什么?
  • 重写整个原型对象后,构造函数的 prototype 指向哪里?实例的[[Prototype]]属性指向哪里?为什么?
  • 原生引用类型的方法是如何创建的?为什么不推荐修改原生引用类型的原型?
  • 原型模式的“共享”本性,在修改包含引用类型的属性时,有怎样的问题?
  • ECMAScript 使用最广泛、认同度最高的创建自定义类型的方法是什么?其原理和优势是什么?
  • 动态原型模式是如何初始化原型的?其对原型的修改有哪些注意点?
  • 寄生构造函数模式的原理是什么?为什么其创建的实例,与其构造函数或构造函数的原型没有关联?
  • 稳妥构造函数模式的原理是什么?为什么该模式比较“稳妥”?
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-11-30

《javascript高级程序设计》学习笔记 | 6.1.理解对象

欢迎关注前端小讴的github,阅读更多原创技术文章

理解对象

  • 面向对象的语言标志:的概念
  • ECMAScript 中没有类的概念
  • ECMAScript 定义对象:无序属性的集合(一组没有特定顺序的值),其属性可以包含基本值对象函数,整个对象可以想象成一个散列表

相关代码 →

  • 创建一个 Object 实例,然后为它添加属性和方法
var person = new Object()
person.name = 'Nicholas'
person.age = 29
person.job = 'Software Engineer'
person.sayName = function () {
  console.log(this.name)
}
  • 对象字面量创建对象
var person = {
  name: 'Nicholas',
  age: 29,
  job: 'Software Engineer',
  sayName: function () {
    console.log(this.name)
  },
}

属性类型

数据属性含义默认值
[[Configurable]]能否通过 delete 删除属性 / 能否修改属性特性 / 能否把属性改为访问器属性直接在对象上定义时:true / 使用 Object.defineProperty()等方法定义时:false
[[Enumerable]]能否通过 for-in 循环返回属性直接在对象上定义时:true / 使用 Object.defineProperty()等方法定义时:false
[[Writable]]能否修改属性的值直接在对象上定义时:true / 使用 Object.defineProperty()等方法定义时:false
[[Value]]属性的数据值undefined
var person = {
  name: 'Nicholas',
}
  • 数据属性包含一个数据值的位置,用 Object.defineProperty() 修改默认属性
  • 方法接收 3 个参数:属性所在的对象、属性名、描述符对象(描述符对象属性必须是:configurable / enumerable / writable / value,一个或多个)
  • 严格模式下,操作会报错
Object.defineProperty(person, 'name', {
  writable: false, // 不可修改
  configurable: false, // 不可配置
  value: 'Nicholas',
})
console.log(person.name)
person.name = 'Greg'
console.log(person.name) // 无法重写
delete person.name
console.log(person.name) // 无法删除
  • 一旦属性定义为不可配置,不能再变回可配置,再次调用 Object.defineProperty() 方法修改除 writable 以外的特性都会报错
Object.defineProperty(person, 'name', {
  configurable: true, // 试图:不可配置 -> 可配置,由于已经改为不可配置,因此会报错
  value: 'Simon',
})
  • 可多次调用 Object.defineProperty() 修改同一属性,一但把 configurable 设置为 false 之后就会有限制了
访问器属性含义默认值
[[Configurable]]能否通过 delete 删除属性 / 能否修改属性特性 / 能否把属性改为数据属性直接在对象上定义时:true / 使用 Object.defineProperty()等方法定义时:false
[[Enumerable]]能否通过 for-in 循环返回属性直接在对象上定义时:true / 使用 Object.defineProperty()等方法定义时:false
[[Get]]读取属性时调用的函数undefined
[[Set]]写入属性时调用的函数undefined
var book = {
  _year: 2004,
  edition: 1,
}
  • 访问器属性不包含数据值,用 Object.defineProperty() 定义属性
  • 方法接收 3 个参数:属性所在的对象、属性名、描述符对象(和数据属性用法一样)
  • 只指定 getter -> 属性不能写;只指定 setter -> 属性不能读
  • 严格模式下,只指定 getter 或 setter 均会报错
Object.defineProperty(book, 'year', {
  get: function () {
    return this._year
  },
  set: function (newValue) {
    if (newValue > 2004) {
      this._year = newValue
      this.edition += newValue - 2004
    }
  },
})

book.year = 2007
console.log(book.edition)
  • IE8 或更早,定义访问器属性的方法(遗留的方法,可在浏览器测试,vscode 会报错)
book.__defineGetter__('year', function () {
  return this._year
})
book.__defineSetter__('year', function (newValue) {
  if (newValue > 2004) {
    this._year = newValue
    this.edition += newValue - 2004
  }
})
book.year = 2008
console.log(book.edition)
  • 调用 Object.defineProperty()、Object.defineProperties()方法时,如果不指定,即非“直接在对象上定义属性”时,那么 configurable、enumerable、writable 均为 false
var book4 = {
  year: 2004,
}
var descriptorBook4 = Object.getOwnPropertyDescriptor(book4, 'year')
console.log(
  'book4',
  descriptorBook4.configurable, // true
  descriptorBook4.enumerable, // true
  descriptorBook4.writable, // true
  typeof descriptorBook4.set, // undefined
  typeof descriptorBook4.get // undefined
)
var book5 = {}
Object.defineProperty(book5, 'year', {
  value: 2004,
})
var descriptorBook5 = Object.getOwnPropertyDescriptor(book5, 'year')
console.log(
  'book4',
  descriptorBook5.configurable, // false
  descriptorBook5.enumerable, // false
  descriptorBook5.writable, // false
  typeof descriptorBook4.set, // undefined
  typeof descriptorBook4.get // undefined
)

定义多个属性

  • Object.defineProperties() 同时定义多个属性
  • 同时定于多个属性,与分别修改和定义对象的属性的唯一却别,是这些属性都是在同一时间创建的
var book2 = {}
Object.defineProperties(book2, {
  _year: {
    writable: true,
    value: 2004,
  },
  edition: {
    writable: true,
    value: 1,
  },
  year: {
    get: function () {
      return this._year
    },
    set: function (newValue) {
      if (newValue > 2004) {
        this._year = newValue
        this.edition += newValue - 2004
      }
    },
  },
})
book2.year = 2008
console.log(book2.edition) // 5

读取属性的特性

  • Object.getOwnPropertyDescriptor() 取得给定属性的描述符
var book3 = {}
Object.defineProperties(book3, {
  _year: {
    value: 2004,
  },
  edition: {
    value: 1,
  },
  year: {
    get: function () {
      return this._year
    },
    set: function (newValue) {
      if (newValue > 2004) {
        this._year = newValue
        this.edition += newValue - 2004
      }
    },
  },
})
var descriptor = Object.getOwnPropertyDescriptor(book3, '_year')
console.log(descriptor)
console.log(descriptor.value) // 2004
console.log(descriptor.configurable) // false,非直接在对象上定义属性,默认值为false
console.log(typeof descriptor.get) // undefined,数据属性不含get函数

var descriptor2 = Object.getOwnPropertyDescriptor(book3, 'year')
console.log(descriptor2)
console.log(descriptor2.value) // undefined,访问器属性不含value
console.log(descriptor2.configurable) // false,非直接在对象上定义属性,默认值为false
console.log(typeof descriptor2.get) // 'function'

总结 & 问点

  • 面向对象的语言的标志是什么?JS 的对象是什么?
  • 创建 JS 对象都有哪些方法?
  • JS 对象有哪些属性类型?其分别有哪些特性?
  • 用哪些方法如何修改或定义 JS 对象的属性?
  • 用哪个方法如何读取对象属性的特性?
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-09-21

《javascript高级程序设计》学习笔记 | 5.7.单体内置对象

欢迎关注前端小讴的github,阅读更多原创技术文章

单体内置对象

相关代码 →

  • 由 ECMAScript 实现提供的,不依赖于宿主环境的对象,在 ECMAScript 程序执行之前就已经存在
  • Global 和 Math

Global 对象

  • 不属于任何其他对象的属性和方法,最终都是 Global 的属性和方法
  • isNan(),isFinite(),parseInt(),parseFloat()
URI 编码方法返回值
encodeURI()URI 编码,冒号、正斜杠、问号、井号除外
encodeURIComponent()URI 编码,所有非标准字符
decodeURI()URI 解码,只针对使用 encode()编码的字符
decodeURIComponent()URI 解码,所有非标准字符
var uri = 'https://element cn/#tab'
console.log(encodeURI(uri)) // https://element%20cn/#tab,本身属于URI的字符不编码(冒号、正斜杠、问号、井号)
console.log(encodeURIComponent(uri)) // https%3A%2F%2Felement%20cn%2F%23tab,编码所有非标准字符
console.log(decodeURI('https%3A%2F%2Felement%20cn%2F%23tab')) // https%3A%2F%2Felement cn%2F%23tab,只针对使用 encode()编码的字符解码
console.log(decodeURIComponent('https%3A%2F%2Felement%20cn%2F%23tab')) // https://element cn/#tab,解码所有非标准字符
eval 方法返回值
eval()将传入的参数当作实际的 EXMAScript 语句解析
  • 将传入的参数当作实际的 EXMAScript 语句解析
  • 被执行的代码具有与该执行环境相同的作用域链
  • eval() 创建的变量或函数不会被提升
  • 严格模式下,外部访问不到 eval() 中创建的变量或函数,为 eval 赋值也会报错
eval("console.log('hi')") // "hi",将传入的参数当作实际的 EXMAScript 语句解析
eval("function sayHi() {console.log('hi')}")
sayHi() // "hi",被执行的代码具有与该执行环境相同的作用域链
// console.log(msg) // 报错,eval() 创建的变量或函数不会被提升
eval("var msg = 'hi'")
console.log(msg) // "hi",被执行的代码具有与该执行环境相同的作用域链
Global 对象属性说明
undefined特殊值 undefined
NaN特殊值 NaN
Infinity特殊值 Infinity
Object构造函数 Object
Array构造函数 Array
Function构造函数 Function
Boolean构造函数 Boolean
String构造函数 String
Number构造函数 Number
Date构造函数 Date
RegExp构造函数 RegExp
Error构造函数 Error
EvalError构造函数 EvalError
RangeError构造函数 RangeError
ReferenceError构造函数 ReferenceError
SyntaxError构造函数 SyntaxError
TypeError构造函数 TypeError
URIError构造函数 URIError
// web浏览器将global全局对象作为window对象
var color = 'red'
function sayColor() {
  console.log(window.color)
}
window.sayColor() // red

Math 对象

max 和 min
min()确定一组数值的最大值
max()确定一组数值的最小值
console.log(Math.max(3, 54, 32, 16)) // 54
console.log(Math.min(3, 54, 32, 16)) // 3
  • 确定数组中的最大值/最小值
var values = [1, 2, 3, 4, 5]
console.log(Math.max.apply(Math, values)) // 把 Math 对象作为 apply()的第一个参数,将数组作为第二个参数
舍入方法
ceil()向上取整
floor()向下取整
round()四舍五入
console.log(Math.ceil(1.9)) // 2
console.log(Math.floor(1.9)) // 1
console.log(Math.round(1.9)) // 2
random 方法
random()返回大于 0 小于 1 的随机数
console.log(Math.random()) // 大于 0 小于 1 的随机数
  • 选择 1-10 之间的整数
console.log(Math.floor(Math.random() * 10 + 1))
  • 选择 m-n 之间的整数
function selectFrom(lowerValue, upperValue) {
  var choices = upperValue - lowerValue + 1 // 获取范围内的数量
  return Math.floor(Math.random() * choices + lowerValue)
}
var num = selectFrom(7, 32)
console.log(num)

总结 & 问点

  • 单体内置对象有哪些?分别有什么特点?
  • Global 对象有哪些方法?分别有什么用法?
  • eval 方法的用法和特点?
    严格模式下,eval 方法受到哪些限制?
  • Global 对象有哪些属性?
  • 在 web 浏览器中,将 global 全局对象作为什么对象实现的?
  • Math 对象有哪些方法?分别有什么用法?
  • 请用 Math 对象确定数组中的最大值/最小值
  • 请用 Math 对象随机生成 m-n 之间的整数
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-08-26

小程序重构 [cnode社区]:mpvue + 开源api,现已上线!

欢迎关注前端小讴的github,阅读更多原创技术文章

闲暇时光表浪费,由于技术栈一直用的vue,因此用 mpVue 开发了小程序版本的 cnode 社区,api 也都是官网开源的。有人会问 “mpvue不是已经停止维护了嘛?”——的确是的,而且使用起来有很有坑。但这是我第一次写小程序,找个最相近的语言最适合——无论选择哪种技术栈,产品要完整做出来才行。小程序现已上线,后续还将逐步尝试在taro、wepy、uni-app等框架开发,毕竟代码需要修改的比较少,争取实现“一套代码,多端通用”

image

欢迎star

https://github.com/simon9124/simon-cnode

效果预览

image
image

查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-08-25

《javascript高级程序设计》学习笔记 | 5.6.基本包装类型

欢迎关注前端小讴的github,阅读更多原创技术文章

基本包装类型

相关代码 →

  • 3 个特殊的引用类型 BooleanNumberString
  • 每读取一个基本类型值,后台就创建一个对应的基本包装类型的对象
var s1 = 'some text'
var s2 = s1.substring(2) // 基本类型不是对象,本不应该有方法

// 因为后台自动完成了下列处理:
var s1 = new String('some text') // 1.创建String类型的实例
var s2 = s1.substring(2) // 2.在实例上调用指定方法
s1 = null // 3.销毁该实例
  • 上述步骤同样适用于 BooleanNumber类型
  • 引用类型和基本包装类型的主要区别在于对象的生存期

    • 引用类型:离开作用域前,对象一直在内存中
    • 基本包装类型:执行完,立即被销毁
var s1 = 'some text'
s1.color = 'red' // 创建String对象
console.log(s1.color) // undefined,s1对象已被销毁,同时再次创建String对象,新对象没有color属性
  • 对基本包装类型的实例调用 typeof 会返回"Object",所有基本包装类型的对象都会被转换为布尔值 true
  • Object 构造函数,根据传入值的类型返回基本包装类型的实例
var objText = new Object('some text') // 创建String的实例
console.log(objText instanceof String) // true
var objBoolean = new Object(false) // 创建Boolean的实例
console.log(objBoolean instanceof Boolean) // true
var objNumber = new Object(123) // 创建Number的实例
console.log(objNumber instanceof Number) // true
console.log(objNumber instanceof Boolean) // false
  • 使用 new 调用基本包装类型的构造函数 vs 直接调用同名的转型函数
var value = '25'
var number = Number(value) // 转型函数,转成Number类型
console.log(typeof number) // number
var obj = new Number(value) // 构造函数,构造Number对象实例
console.log(typeof obj) // object

Boolean 类型

var booleanObject = new Boolean(true) // 调用Boolean构造函数,并传入true/false
重写(继承)的方法返回值
valueOf()Boolean true / false
toString()String "true" / "false"
  • 基本类型布尔值 vs 引用类型布尔值:

    • typeof 操作符对基本类型返回 boolean,对引用类型返回 object
    • instance 操作符对基本类型返回 false,对引用类型返回 true
var falseObj = new Boolean(false) // falseObj是基本包装类型,被转换成true(所有基本包装类型对象都会被转换成true)
var falseValue = false // falseValue是基本类型,就是false

console.log(falseObj && true) // true
console.log(falseValue && true) // false
console.log(typeof falseObj) // object,基本包装类型
console.log(typeof falseValue) // boolean,基本类型
console.log(falseObj instanceof Boolean) // true,Boolean对象是Boolean类型的实例
console.log(falseValue instanceof Boolean) // false

Number 类型

var numberObject = new Number(10) // 调用Number构造函数,并传入数值
重写(继承)的方法返回值
valueOf()Number 数值
toLocaleString()String 字符串形式的数值
toString()String 字符串形式的数值
  • 为 toString()方法传递表示基数的参数,告诉返回几进制的字符串形式
var num1 = 10
console.log(num1.toString()) // 十进制,"10"
console.log(num1.toString(2)) // 二进制,"1010"
console.log(num1.toString(8)) // 八进制,"12"
console.log(num1.toString(10)) // 十进制,"10"
console.log(num1.toString(16)) // 十六进制,"a"
格式化字符串方法返回值
toFixed(num)String 指定小数位
toExponential(num)String 指数表示法
toPrecision(num)String 返回 fixed 或 exponential 格式
var num2 = 10000
console.log(num2.toFixed(2)) // 指定小数位,"10000.00"
console.log(num2.toExponential(2)) // 指数表示法,"1.00e+4"

var num3 = 99
console.log(num3.toPrecision(1)) // 用一位数表示,"1e+2"
console.log(num3.toPrecision(2)) // 用二位数表示,"99"
console.log(num3.toPrecision(3)) // 用三位数表示,"99.0"
  • 基本类型数值 vs 引用类型数值:

    • typeof 操作符对基本类型返回 number,对引用类型返回 object
    • instance 操作符对基本类型返回 false,对引用类型返回 true
var numberObject = new Number(10)
var numberValue = 10
console.log(typeof numberObject) // object,基本包装类型
console.log(typeof numberValue) // value,基本类型
console.log(numberObject instanceof Number) // true,Number对象是Number类型的实例
console.log(numberValue instanceof Number) // false

String 类型

var StringObject = new String('hello world') // 调用String构造函数,并传入字符串
重写(继承)的方法返回值
valueOf()String 字符串值
toLocaleString()String 字符串值
toString()String 字符串值
  • String 类型每个实例都有 length 属性,表示字符串包含的字符数量,双节字符也算作 1 个字符
console.log(stringObject.length) // "11"
字符方法返回值
charAt(num)给定位置的字符
charCodeAt()给定位置的字符编码
console.log(stringObject.charAt(1)) // "e"
console.log(stringObject.charCodeAt(1)) // "101"
console.log(stringObject[1]) // "e"
字符串操作方法返回值
concat()拼接得到的字符串
slice(num1,num2)被操作字符串的子字符串
substring(num1,num2)被操作字符串的子字符串
substr(num1,num2)被操作字符串的子字符串
console.log(stringObject.concat(' wonderful')) // "hello world wonderful"
console.log(stringObject.slice(3)) // "lo world"
console.log(stringObject.substring(3)) // "lo world"
console.log(stringObject.substr(3)) // "lo world"
console.log(stringObject.slice(3, 7)) // "lo w",第二个参数指定到哪里结束(不包含)
console.log(stringObject.substring(3, 7)) // "lo w",第二个参数指定到哪里结束(不包含)
console.log(stringObject.substr(3, 7)) // "lo worl",第二个参数指定要返回的字符个数
console.log(stringObject.slice(-3)) // "rld",-3转换为11-3=8
console.log(stringObject.substring(-3)) // "hello world",-3转换为0
console.log(stringObject.substr(-3)) // "rld",-3转换为11-3=8
console.log(stringObject.slice(3, -4)) // "lo w",-4转换为11-3=7
console.log(stringObject.substring(3, -4)) // "lo w",-4转换为0,substring(3,0)再转换为substring(0,3)
console.log(stringObject.substr(3, -4)) // "",-4转换为0,返回包含零个字符的字符串
字符串位置方法返回值
indexOf()子字符串的位置,前 → 后搜索
lastIndexOf()子字符串的位置,后 → 前搜索
console.log(stringObject.indexOf('o')) // 4
console.log(stringObject.lastIndexOf('o')) // 7
console.log(stringObject.indexOf('o', 6)) // 7,第二个参数表示从哪个位置开始搜索
console.log(stringObject.lastIndexOf('o', 6)) // 4,第二个参数表示从哪个位置开始搜索

var loremString =
  'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'
var position = new Array()
var pos = loremString.indexOf('o')
while (pos > -1) {
  position.push(pos)
  pos = loremString.indexOf('o', pos + 1)
}
console.log(position) // 所有包含o的字符位置集合
trim()方法返回值
trim()创建副本,删除前置后缀空格,返回副本
trimLeft()创建副本,删除前置空格,返回副本
trimRight()创建副本,删除后缀空格,返回副本
var stringValue = '  hello world  '
var trimStringValue = stringValue.trim()
console.log(stringValue) // "  hello world  "
console.log(trimStringValue) // "hello world"
字符串大小写转换方法返回值
toLowerCase()转小写
toLocaleLowerCase()根据地区转小写
toUpperCase()转大写
toLocaleUpperCase()根据地区转大写
console.log(stringObject.toUpperCase()) // "HELLO WORLD"
console.log(stringObject.toLocaleUpperCase()) // "HELLO WORLD"
console.log(stringObject.toLowerCase()) // "hello world"
console.log(stringObject.toLocaleLowerCase()) // "hello world"
字符串模式匹配方法返回值
match()数组
search()首个匹配项索引,没有返回-1
replace()替换后的字符串
split()分割成多个字符串,放在数组中
var text = 'cat, bat, sat, fat'
var pattern = /.at/

var matches = text.match(pattern)
console.log(matches) // [ 'cat',index: 0,input: 'cat, bat, sat, fat',groups: undefined ]
// match方法返回一个数组,第一项与整个模式匹配的字符串,后面的项保存着与正则表达式捕获匹配的字符串
console.log(matches.index) // 0
console.log(matches[0]) // "cat"
console.log(pattern.lastIndex) // 0

var pos = text.search(/at/)
console.log(pos) // 1,记录"at"首次在字符串出现的位置

var result = text.replace('at', 'ond')
console.log(result) // "cond, bat, sat, fat",第一个参数是字符串所以只替换第一个匹配的
result = text.replace(/at/g, 'ond')
console.log(result) // "cond, bond, sond, fond",第一个参数是全局正则所以替换全部的
result = text.replace(/(.at)/g, 'word($1)')
console.log(result) // "word(cat), word(bat), word(sat), word(fat)",$1表示第一个匹配的参数

function htmlEscape(text) {
  return text.replace(/[<>"&]/g, function (match, pos, originalText) {
    switch (match) {
      case '<':
        return '&lt;'
        break
      case '>':
        return '&gt;'
        break
      case '&':
        return '&amp;'
        break
      case '"':
        return '&quot;'
        break
    }
  })
}
console.log(htmlEscape('<p class="greeting">Hello world!</p>')) // "&lt;p class=&quot;greeting&quot;&gt;Hello world!&lt;/p&gt;"

console.log(text.split(',')) // [ 'cat', ' bat', ' sat', ' fat' ]
console.log(text.split(',', 2)) // [ 'cat', ' bat' ],第二个参数指定数组大小
console.log(text.split(/[^\,]+/)) // [ '', ',', ',', ',', '' ],通过正则获取包含都好字符的数组,正则指定分隔符出现在了字符串开头和末尾,因此首位是空字符串
localeCompare()方法返回值
localeCompare()字符串参数在字母表中的前后关系
var stringCompare = 'yellow'
console.log(stringCompare.localeCompare('brick')) // 1,brick字母表在yellow之前
console.log(stringCompare.localeCompare('yellow')) // 0,yellow字母表与yellow相同
console.log(stringCompare.localeCompare('zoo')) // -1,zoo字母表在yellow之后

function determineOrder(value) {
  var result = stringCompare.localeCompare(value)
  result < 0 &&
    console.log(`The string 'yellow' comes before the string ${value}.`)
  result > 0 &&
    console.log(`The string 'yellow' comes after the string ${value}.`)
  result === 0 &&
    console.log(`The string 'yellow' is equal to the string ${value}.`)
}
determineOrder('brick') // "The string 'yellow' comes after the string brick."
determineOrder('yellow') // "The string 'yellow' is equal to the string zoo."
determineOrder('zoo') // "The string 'yellow' comes before the string zoo."
fromCharCode()方法返回值
fromCharCode()转换后的字符串
// 将若干字符编码转换成字符串,与charCodeAt()相反
console.log(String.fromCharCode(104, 101, 108, 108, 111)) // "hello"
html 方法返回值
anchor(name)<a name="name">string</a>
big()<big>string</big>
bold()<b>string</b>
fixed()<tt>string</tt>
fontcolor(color)<font color="color">string</font>
fontsize(size)<font size="size">string</font>
italics()<i>string</i>
link(url)<a herf="url">string</a>
small()<small>string</small>
strike()<strike>string</strike>
sub()<sub>string</sub>
sup()<sup>string</sup>

总结 & 问点

  • 基本包装类型包含哪些特殊的引用类型?读取时后台经历了什么步骤?
  • 引用类型和基本包装类型的主要区别是什么?
  • 对基本包装类型的实例调用 typeof 会返回什么?基本包装类型的对象都会被转换为什么?
  • 请使用 Object 构造函数判断基本包装类型的实例
  • 使用 new 调用基本包装类型的构造函数与直接调用同名转型函数的区别是什么?
  • Boolean 的引用类型重写了哪些方法?其基本类型和引用类型的区别是什么?
  • Number 的引用类型重写了哪些方法?提供了哪些格式化字符串方法?其基本类型和引用类型的区别是什么?
  • String 的引用类型重写了哪些方法?提供了哪些字符串操作或解析方法?其基本类型和引用类型的区别是什么?
  • 请说明 charAt()、charCodeAt()、concat()、slice()、substr()、substring()、indexOf()、lastIndexOf()、trim()、toLowerCase()、toUpperCase()、match()、search()、replace()、split()、localeCompare()、fromCharCode()等方法的含义,并举出相关例子
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-08-07

解决 mpvue 经典 bug:同路由切换时,上次的数据会保留

关注前端小讴,阅读更多原创技术文章

由于一直在用vue技术栈,因此初次开发小程序尝试使用【mpvue】。尽管听闻框架已停止维护,为了上手迅速,不顾可能存在的众多bug,毅然用起来。

果不其然,一个大bug出现了同一路由切换时,上一次的页面数据会保留

项目实战bug:mpvue重构cnode社区
image

github用户已在mpvue的issues给出相关原因:
image

参考众多答案后,用“建页面栈”的办法得以解决

const dataStack = []; // 建一个页面栈

export default {
  data () {
    return {...};
  },
  onLoad () {
    dataStack.push({ ...this.$data }); // 备份数据
    // 其他初始化....一定要先备份,再做其他初始化
  },
  onUnload () {
    Object.assign(this.$data, dataStack.pop()); // 恢复数据
  }
}

由于 vue 的 mixin 中的代码先于页面执行,因此上述方法可优化到 mixin.js 文件中

let mixin = {
  data() {
    return {
      dataStack: [], // 解决mpvue相同组件数据不更新问题,建立栈堆
    };
  },
  onUnload() {
    Object.assign(this.$data, this.dataStack.pop()); // 恢复
  },
  onLoad() {
    this.dataStack.push({ ...JSON.parse(JSON.stringify(this.$data)) }); // 备份
  },
};
export default mixin;

bug得以解决:
image

功能和项目完整代码参见simon-cnode(mpvue重构cnode社区),欢迎star!

查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-07-30

手摸手,封装一个vue分页组件

关注前端小讴,阅读更多原创技术文章

【组件化】是每一个前端工程师的必备技能,诚然我们将element、iview、vant等UI组件库运用得娴熟自如,实际开发中还是经常需要封装更适合的业务组件,既帮助快速开发、又让代码简洁明了、还能锻炼我们的组件化能力。

梳理思路

node.js官网的分页组件为例,假设我们要实现下面这样的分页:
最终截图.png
最终截图2.png
最终截图3.png
最终截图4.png

从产品角度梳理思路后,要实现的分页组件有以下特点:
1.当前页页码颜色变化
2.最多显示5个页码,不足5个显示实际数量
3.总页数超过5个时:总页码-当前页>2,则末尾显示省略号;当前页>2,则开头显示省略号
4.总页数超过5个时:当前页为最后1页,则一共显示3个页码;当前页为倒数第二页,则一共显示4个页码;其余均显示5个页码
5.总页数超过5个、且同时满足总页码-当前页>2当前页>2时,当前页总是显示在5个页码的最中间
6.点击最左、最右侧箭头,分别跳转到第1、最后1页

基本结构

先写一个静态的结构:

<template>
  <div class="page">
    <span class="page-block center">{{'«'}}</span>
    <span v-if="pageList[2]-2>1"
          class="page-block center">...</span>
    <span v-for="pageNum in pageList"
          :key="pageNum"
          class="page-block center"
          :style="{color:pageNum===page?'#80bd01':'#778087'}">{{pageNum}}</span>
    <span v-if="pageMax-pageList[2]>2"
          class="page-block center">...</span>
    <span class="page-block center">{{'»'}}</span>
  </div>
</template>

<script>
export default {
  data () {
    return {
      pageList: [1,2,3,4,5], // 页码列表
      page: 1, // 当前页码
      pageMax: 7 // 最大页数
    };
  }
};
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.page {
  padding: 10px;
  background-color: #fff;

  &-block {
    display: inline-block;
    width: 30px;
    height: 28px;
    padding: 4px 8px;
    font-size: 0.8em;
    line-height: 18px;
    border: 1px solid #ddd;
    border-right: none;
    box-sizing: border-box;

    &:first-child {
      width: 34px;
      border-top-left-radius: 4px;
      border-bottom-left-radius: 4px;
    }

    &:last-child {
      width: 34px;
      border-right: 1px solid #ddd;
      border-top-right-radius: 4px;
      border-bottom-right-radius: 4px;
    }
  }
}
</style>

在父组件引用该组件:

<template>
  <pagination></pagination>
</template>

<script>
import Pagination from "@/components/pagination";
export default {
  components: { Pagination }
}
</script>

此时效果:将看到一个标准的、大于5页的静态分页:
最终截图.png

props接收值

组件的某些值应该是父组件传递过来的,通过计算后挂载

props: {
  // 内容总数
  total: {
    type: Number,
    default: 0
  },
  // 每页数量
  limit: {
    type: Number,
    default: 10
  },
  // 当前页码
  page: {
    type: Number,
    default: 1
  },
},
data () {
  return {
    pageList: [] // 页码列表
  };
},
computed: {
  // 最大页数
  pageMax () {
    return Math.ceil(this.total / this.limit);
  },
},
onLoad () { // 我的框架是mpvue,vue用created
  this.initData();
},
methods: {
  // 生成pageList页码列表
  initData () {
    this.pageList = []; // 清空页码
    var i = 1;
    do {
      this.pageList.push(i);
      i++;
    } while (i <= this.pageMax);
    this.pageList.length > 5 && (this.pageList = this.pageList.slice(0, 5)); // 最多显示5页
  }
}

父组件传递这些值:

<pagination :total="dataList.length"
            :limit="limit"
            :page="page"></pagination>
data () {
  return {
    dataList: [], // 列表
    page: 1, // 当前页码
    limit: 20, // 每页数量
  };
},
onLoad () {
  this.getData();
},
methods: {
  getData () {
     // 动态获取列表 - 以后端分页为例
     this.dataList = (await getDataList(this.limit, this.page)).data;
  }
}

此时效果:将根据父组件——也就是使用该组件的页面——的真实数据,渲染分页组件。页码数量大于5不大于5组件内容不同,且当前页码为首页

event事件回调

组件中追加事件,回调给父组件(调用的组件):

<template>
  <div class="page">
    <span class="page-block center"
          @click="pageChange(1)">{{'«'}}</span>
    <span v-if="pageList[2]-2>1"
          class="page-block center">...</span>
    <span v-for="pageNum in pageList"
          :key="pageNum"
          class="page-block center"
          :style="{color:pageNum===page?'#80bd01':'#778087'}"
          @click="pageChange(pageNum)">{{pageNum}}</span>
    <span v-if="pageMax-pageList[2]>2"
          class="page-block center">...</span>
    <span class="page-block center"
          @click="pageChange(pageMax)">{{'»'}}</span>
  </div>
</template>
methods: {
  // 子组件事件回调:分页
  pageChange (page) {
    this.$emit("page-change", page);
  },
}

父组件执行子组件回调过来的事件:

<pagination :total="dataList.length"
            :limit="limit"
            :page="page"
            @page-change="pageChange"></pagination>
methods: {
  // 分页
  pageChange (page) {
    this.page = page;
    this.getData();
  },
}

此时效果:点击页码颜色会发生变化,父组件接收到子组件的回调事件,返回一个值page——即当前页码,可根据页码做分页的内容渲染;点击最左、最右侧箭头,分别跳转到第1、最后1页

watch页码变化

由于不同页码要渲染的组件内容不同,因此需要监听页码变化,刷新组件内容;同时total总数更新时,重新加载组件

watch: {
  // 监听页码变化 -> 页码列表更新
  page (val) {
    if (val <= 3) {
      this.pageList = [];
      var i = 1;
      do {
        this.pageList.push(i);
        i++;
      } while (i <= this.pageMax);
      this.pageList.length > 5 && (this.pageList = this.pageList.slice(0, 5)); // 最多显示5页
    } else if (val === this.pageMax) {
      this.pageList = [val - 2, val - 1, val];
    } else if (val === this.pageMax - 1) {
      this.pageList = [val - 2, val - 1, val, val + 1];
    } else {
      this.pageList = [val - 2, val - 1, val, val + 1, val + 2];
    }
  },
  // 监听页码变化 -> 总数更新
  total (val) {
    this.initData();
  }
}

此时效果:组件根据页码变化数量变化,渲染出不同的内容,至此该分页组件全部内容完成

扩展

可以像element和iview那样,为组件追加当前页显示数量下拉框,也可以由父组件决定子组件的样式布局,页码按钮的数量(本例为最大5个)。。。总体思路不变,即:
1.子组件接收值并根据值而改变渲染
2.子组件追加事件,回调给父组件
3.根据需要追加监听

完整代码

<template>
  <div class="page">
    <span class="page-block center"
          @click="pageChange(1)">{{'«'}}</span>
    <span v-if="pageList[2]-2>1"
          class="page-block center">...</span>
    <span v-for="pageNum in pageList"
          :key="pageNum"
          class="page-block center"
          :style="{color:pageNum===page?'#80bd01':'#778087'}"
          @click="pageChange(pageNum)">{{pageNum}}</span>
    <span v-if="pageMax-pageList[2]>2"
          class="page-block center">...</span>
    <span class="page-block center"
          @click="pageChange(pageMax)">{{'»'}}</span>
  </div>
</template>

<script>
export default {
  props: {
    // 内容总数
    total: {
      type: Number,
      default: 0
    },
    // 每页数量
    limit: {
      type: Number,
      default: 10
    },
    // 当前页码
    page: {
      type: Number,
      default: 1
    },
  },
  data () {
    return {
      pageList: [] // 页码列表
    };
  },
  computed: {
    // 最大页数
    pageMax () {
      return Math.ceil(this.total / this.limit);
    },
  },
  onLoad () {
    this.initData();
  },
  methods: {
    initData () {
      this.pageList = []; // 清空页码
      var i = 1;
      do {
        this.pageList.push(i);
        i++;
      } while (i <= this.pageMax);
      this.pageList.length > 5 && // 最多显示5页
        (this.pageList = this.pageList.slice(0, 5));
    },
    // 子组件事件回调:分页
    pageChange (pageCurrent) {
      this.$emit("page-change", pageCurrent);
    },
  },
  watch: {
    // 监听页码变化 -> 页码列表更新
    page (val) {
      if (val <= 3) {
        this.pageList = [];
        var i = 1;
        do {
          this.pageList.push(i);
          i++;
        } while (i <= this.pageMax);
        this.pageList.length > 5 && // 最多显示5页
          (this.pageList = this.pageList.slice(0, 5));
      } else if (val === this.pageMax) {
        this.pageList = [val - 2, val - 1, val];
      } else if (val === this.pageMax - 1) {
        this.pageList = [val - 2, val - 1, val, val + 1];
      } else {
        this.pageList = [val - 2, val - 1, val, val + 1, val + 2];
      }
    },
    // 监听页码变化 -> 总数更新
    total (val) {
      this.initData();
    }
  }
};
</script>

<style rel="stylesheet/scss" lang="scss" scoped>
.page {
  padding: 10px;
  background-color: #fff;

  &-block {
    display: inline-block;
    width: 30px;
    height: 28px;
    padding: 4px 8px;
    font-size: 0.8em;
    line-height: 18px;
    border: 1px solid #ddd;
    border-right: none;
    box-sizing: border-box;

    &:first-child {
      width: 34px;
      border-top-left-radius: 4px;
      border-bottom-left-radius: 4px;
    }

    &:last-child {
      width: 34px;
      border-right: 1px solid #ddd;
      border-top-right-radius: 4px;
      border-bottom-right-radius: 4px;
    }
  }
}
</style>

api

props:
属性说明类型默认值
total内容总数Number0
limit每页数量Number10
page当前页码Number1
events:
事件名说明返回值
page-change页码改变的回调,返回改变后的页码当前页码
查看原文

赞 0 收藏 0 评论 0

小讴 发布了文章 · 2020-07-20

《javascript高级程序设计》学习笔记 | 5.5.Function类型

欢迎关注前端小讴的github,阅读更多原创技术文章

Function 类型

  • 函数是对象,每个函数都是 Function 类型的实例,都与其他引用类型一样具有属性方法
  • 函数名是指向函数对象的指针,不会与某个函数绑定(一个函数可能会有多个名字)

相关代码 →

3 种声明方式

// 1.函数声明定义
function sum(num1, num2) {
  return num1 + num2
}

// 2.函数表达式定义
var sum = function (num1, num2) {
  return num1 + num2
}

// 3.Function构造函数定义
/* 不推荐使用构造函数定义,因为会造成解析2次代码(1次解析常规js代码,1次解析传入构造函数中的字符串) */
var sum = new Function('num1', 'num2', 'return num1+num2')

访问函数指针

function sum(num1, num2) {
  return num1 + num2
}
console.log(sum(10, 10))

var anotherSum = sum // 使用不带圆括号的函数名是访问函数指针,而非调用函数
console.log(anotherSum(10, 10))

/* sum 和 anotherSum 同时指向同一个函数 */

sum = null // sum与函数断绝关系
console.log(anotherSum(10, 10)) // 但anotherSum()仍可正常调用
console.log(sum(10, 10)) // 会报错,sum is not a function

没有重载

function addSomeNumber(num) {
  return num + 100
}
// 创建第二个函数
function addSomeNumber(num) {
  return num + 200
}
// 第二个函数等价于下列代码 -> 覆盖了引用第一个函数的变量addSomeNumber
addSomeNumber = function (num) {
  return num + 200
}
var result = addSomeNumber(100)
console.log(result) // 300

函数声明与函数表达式

js 引擎在代码开始执行之前,解析器通过函数声明提升(function declaration hoisting)的过程,将声明函数放到源代码树的顶部,使其在执行任何代码之前可用(可以访问);而函数表达式则必须等到解析器执行到所在代码行才被解释执行。

函数声明函数表达式的唯一区别就是什么时候可以通过变量访问函数

console.log(sumDeclare(10, 10)) // 函数声明会提前
function sumDeclare(num1, num2) {
  return num1 + num2
}
console.log(sumExpression(10, 10)) // 函数表达式不会提前,会报错,sumExpression is not a function
var sumExpression = function (num1, num2) {
  return num1 + num2
}

作为值的函数

像传递参数一样,把一个函数传递给另一个函数,也可以将一个函数作为另一个函数的结果返回

function callSomeFunction(someFunction, someArgument) {
  return someFunction(someArgument)
}

function add10(num) {
  return num + 10
}
var result1 = callSomeFunction(add10, 10) // 访问函数的指针而不是执行函数
console.log(result1) // 20

function getGreeting(name) {
  return 'Hello,' + name
}
var result2 = callSomeFunction(getGreeting, 'Nicholas') // 访问函数的指针而不是执行函数
console.log(result2) // Hello,Nicholas

【例】想要根据数组对象的某个对象属性进行排序:

/**
 * 按照对象数组的某个object key,进行数组排序
 * @param {String} key 要排序的key
 * @param {String} sort 正序/倒序:asc/desc,默认为asc
 */
function arraySort(key, sort) {
  return function (a, b) {
    if (sort === 'asc' || sort === undefined || sort === '') {
      // 正序:a[key] > b[key]
      if (a[key] > b[key]) return 1
      else if (a[key] < b[key]) return -1
      else return 0
    } else if (sort === 'desc') {
      // 倒序:a[key] < b[key]
      if (a[key] < b[key]) return 1
      else if (a[key] > b[key]) return -1
      else return 0
    }
  }
}
var userList = [
  { name: 'Tony', id: 3 },
  { name: 'Tom', id: 2 },
  { name: 'Jack', id: 5 },
]
console.log(userList.sort(arraySort('id'))) // 按 id 正序排列
console.log(userList.sort(arraySort('id', 'desc'))) // 按 id 倒序排列

函数内部属性

函数内部有 2 个特殊对象 argumentsthis,1 个内部属性 caller

arguments 是一个类数组对象,保存着函数的所有参数。对象有 callee 属性,该属性是一个指针,指向拥有这个 arguments 对象的函数

// 递归函数:计算阶乘
function factorial(num) {
  if (num <= 1) return 1
  else return num * factorial(num - 1)
}
// 内部不再引用函数名,无论函数名是什么,都可以保证完成递归调用
function factorial(num) {
  if (num <= 1) return 1
  else return num * arguments.callee(num - 1) // callee指向拥有这个arguments对象的函数
}

var trueFactorial = factorial // 保存函数的指针
factorial = function () {
  return 0
}
console.log(trueFactorial(5)) // 120,已用arguments.callee解除函数体内代码与函数名的耦合,仍能正常计算
console.log(factorial(5)) // 0

this 引用的是函数执行的环境对象,即 this 值(全局作用域调用函数时引用的是 window

// vscode是node运行环境,无法识别全局对象window,测试时需做微调
window.color = 'red'
var o = { color: 'blue' }
function sayColor() {
  console.log(this.color)
}
sayColor() // red,此时this指向对象window
o.sayColor = sayColor
o.sayColor() // blue,此时this指向对象o

ES5 定义了 caller 属性,保存着调用当前函数的函数引用—— 谁调用了当前函数,caller 就是谁(全局作用域中调用当前函数 caller 值为 null)

function callerTest() {
  console.log(callerTest.caller)
}
callerTest() // 在全局作用域种调用当前函数,caller 的值为 null

function outer() {
  inner()
}
function inner() {
  console.log(inner.caller)
}
outer() // outer()调用了inner,因此打印outer()函数的源代码

function inner() {
  console.log(arguments.callee.caller) // 可以解除紧密耦合
}
outer() // 结果不变,打印outer()函数的源代码
  • 函数在严格模式下运行时,访问 arguments.callee 会报错
  • ES5 定义了 arguments.caller 属性,严格模式会报错,非严格模式始终是 undefined,该属性是为了区分 arguments.caller 和 函数的 caller
  • 严格模式不能为函数的 caller 属性赋值,会报错

函数属性和方法

每个函数都包含 2 个属性:lengthprototype

length 表示函数希望接收的命名参数的个数

function nameLength(name) {
  return name
}
function sumLength(sum1, sum2) {
  return sum1 + sum2
}
function helloLength() {
  return 'Hello'
}
console.log(nameLength.length, sumLength.length, helloLength.length) // 1,2,0

prototype 保存着函数所有实例方法不可枚举(使用for-in无法发现),每个函数都包含 3 个非继承而来的方法apply()call()bind()

apply()call()的用途和结果相同 —— 都是在特定的作用域中调用函数,apply() 接收 arguments 对象或数组实例,call() 接收每一个参数

function sumPrototype(num1, num2) {
  return num1 + num2
}

apply()接收 2 个参数:① 运行函数的作用域;② 参数数组(实例或 arguments 对象均可)

function applySum1(num1, num2) {
  return sumPrototype.apply(this, arguments) // 传入arguments对象
}
function applySum2(num1, num2) {
  return sumPrototype.apply(this, [num1, num2]) // 传入数组实例
}
console.log(applySum1(10, 10))
console.log(applySum2(10, 10))

call()接收若干参数:① 运行函数的作用域;剩余参数分别传入

function callSum(num1, num2) {
  return sumPrototype.call(this, num1, num2) // 分别传入每个参数
}
console.log(callSum(10, 10))

apply()call() 真正强大的地方在于能够扩充函数运行的作用域

// vscode是node运行环境,无法识别全局对象window,测试时需做微调
window.color = 'red'
var o = { color: 'blue' }
function sayColor() {
  console.log(this.color)
}
sayColor() // red,此时this指向对象window
sayColor().call(this) // red,此时this指向对象window
sayColor().call(window) // red,此时this指向对象window
sayColor().call(o) // blue,此时this指向对象o

ES5 追加 bind() 方法,其创建一个函数实例,其 this 被绑定到传给 bind() 函数的值

// vscode是node运行环境,无法识别全局对象window,测试时需做微调
window.color = 'red'
var o = { color: 'blue' }
function sayColor() {
  console.log(this.color)
}
var bindColor = sayColor.bind(o)
bindColor() // blue,此时this被绑定给对象o

总结 & 问点

  • 函数是什么?函数名是什么?
  • 函数分别有哪几种声明方式?
  • js 的函数有重载嘛?为什么?
  • 函数声明与函数表达式有什么区别?
  • 什么是函数的声明提前?
  • 请写出 function:根据对象数组的某个对象进行排序
  • 函数内部有哪些对象和属性?这些对象和属性分别有哪些特点或用法?
  • 严格模式下,函数内部的对象或属性分别会有哪些限制?
  • 函数有哪些属性?其实例方法都保存在哪里?
  • 函数有哪些非继承而来的方法?分别有什么用法和作用?
查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 53 次点赞
  • 获得 4 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • iview-dynamicRouter

    iView Admin的后端动态生成路由模板,内置“权限管理”业务模型,可任意调整左侧菜单栏、修改其相关权限、监听当前路由和菜单,是一套更安全、更智能的后台管理系统模板。

  • github博客

    深入前端核心理论,记录个人博客

注册于 2017-12-11
个人主页被 1.7k 人浏览