五、JavaScript函数和对象的增强知识

2022/7/28 JS高级函数对象属性length/namearguments函数的剩余参数...args纯函数柯里化自动柯里化函数组合函数with语句eval函数严格模式对象的增强知识Object.defineProperty()数据属性描述符存储属性描述符Object.getOwnPropertyDescriptor()

# 1、函数对象的属性

  • 我们知道JavaScript中函数也是一个对象,那么对象中就可以有属性和方法。

    • 属性name:一个函数的名词我们可以通过name来访问;

    • 属性length:属性length用于返回函数形参的个数;(具体细节如下)

      • 如果函数的形参中有的设置了默认值,那么是不参与参数的个数的;

        • 注意:从左至右,找到第一个设置了默认值的形参,那么这个形参及后面的形参都是不参与参数的个数的(下面案例2可以说明)
      • rest参数是不参与参数的个数的;(rest参数是ES6引入的参数;形式为:...变量名)下面案例指:...arg参数

//案例1.如果函数的形参中有的设置了默认值,那么是不参与参数的个数的;
function foo(a,b,c=6,d=9,...args) {
    console.log('foo1执行了')
}
console.log(foo.name)//foo
console.log(foo.length)//2

//案例2.注意:从左至右,找到第一个设置了默认值的形参,那么这个形参及后面的形参都是不参与参数的个数的
function foo1(a,b=6,c,d=9,e,...args) {
    console.log('foo执行了')
}
console.log(foo1.length)//1

//案例3.rest参数是不参与参数的个数的;
function foo2(a,b,c,d,...args) {
    console.log('foo2执行了')
}
console.log(foo2.length)//4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 2、arguments

# 2.1.认识arguments

  • arguments 是一个 对应于 传递给函数的参数 的 类数组(array-like)对象。
function foo(x, y, z) {
  console.log(arguments)//Arguments(3) [1, 2, 3, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
foo(1,2,3)
1
2
3
4
  • array-like意味着它不是一个数组类型,而是一个对象类型:
    • 但是它却拥有数组的一些特性,比如说length,比如可以通过index索引来访问;
    • 但是它却没有数组的一些方法,比如filter、map等;
function foo(x, y, z=2) {
  console.log(arguments[0])//1
  console.log(arguments.length)//3
  console.log(arguments.find(item => item===1))// 报错:arguments.find is not a function
}
foo(1,2,3)
1
2
3
4
5
6
  • javascript中常见的类数组有arguments对象和DOM方法的返回结果。
    • 比如Array.from(document.querySelectorAll('div'))

# 2.2.arguments转Array

  • 在开发中,我们经常需要将arguments转成Array,以便使用数组的一些特性。
  • 常见的转化方式如下
    • 转化方式一:遍历arguments,添加到一个新数组中;
    • 转化方式二:较难理解(有点绕)
      • 调用数组slice函数的call方法;[].slice.call(arguments)Array.prototype.slice.call(arguments)
    • 转化方式三:ES6中的两个方法
      • Array.from()
      • […arguments]
function foo(a,b,c) {
  console.log(arguments)//Arguments(3) ["nba", "cba", "ncaa", callee: ƒ, Symbol(Symbol.iterator): ƒ]
  var newArr1 = []
  // 转化方式一:遍历arguments,添加到一个新数组中;
  for(var i=0;i<arguments.length;i++) {
    newArr1.push(arguments[i])
  }
  console.log(newArr1)//(3) ["nba", "cba", "ncaa"]

  // 转化方式二:调用数组slice函数的call方法;
  var newArr2 = [].slice.call(arguments)
  console.log(newArr2)//(3) ["nba", "cba", "ncaa"]
  var newArr3 = Array.prototype.slice.call(arguments)
  console.log(newArr3)//(3) ["nba", "cba", "ncaa"]

  // 转化方式三:ES6中的两个方法
  var newArr4 = [...arguments]//...迭代数组的内容并展示
  console.log(newArr4)//(3) ["nba", "cba", "ncaa"]
  var newArr5 = Array.from(arguments)
  console.log(newArr5)//(3) ["nba", "cba", "ncaa"]
}

foo('nba','cba','ncaa')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.3.箭头函数不绑定arguments

  • 箭头函数是不绑定arguments的,所以我们在箭头函数中使用arguments会去上层作用域查找:
function fn(n,m) {
  return (x) => {
    console.log(arguments)//Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
  }
}
var bar = fn(1,2)
bar(1)
1
2
3
4
5
6
7

# 3、函数的剩余(rest)参数

  • ES6中引用了rest parameter,可以将不定数量的参数放入到一个数组中:
    • 如果最后一个参数是 ... 为前缀的,那么它会将剩余的参数放到该参数中,并且作为一个数组;
function foo(m,n,...args) {
  console.log(m,n)//1,2
  console.log(args)//[3,4,5,6,7]
}
foo(1,2,3,4,5,6,7)
1
2
3
4
5
  • 那么剩余参数和arguments有什么区别呢?

    • 剩余参数只包含那些没有对应形参的实参,而 arguments 对象包含了传给函数的所有实参;
    • arguments对象不是一个真正的数组,而rest参数是一个真正的数组,可以进行数组的所有操作;
    • arguments是早期的ECMAScript中为了方便去获取所有的参数提供的一个数据结构,而rest参数是ES6中提供并且希望以此来替代arguments的;
  • 剩余参数必须放到最后一个位置,否则会报错。function foo(...arg,m,n){} 该函数调用会报错

# 4、JavaScript纯函数

# 4.1.理解JavaScript纯函数

  • 函数式编程中有一个非常重要的概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念

    • 在react开发中纯函数是被多次提及的;
    • 比如react中组件就被要求像是一个纯函数(为什么是像,因为还有class组件),redux中有一个reducer的概念,也是要求必须是一个纯函数;
    • 所以掌握纯函数对于理解很多框架的设计是非常有帮助的;
  • 纯函数的维基百科定义:在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:

    • 此函数在相同的输入值时,需产生相同的输出。
    • 函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的外部输出无关。
    • 该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
  • 当然上面的定义会过于的晦涩,所以我简单总结一下:

    • 确定的输入,一定会产生确定的输出;
    • 函数在执行过程中,不能产生副作用;

# 4.2.副作用概念的理解

  • 那么这里又有一个概念,叫做副作用,什么又是副作用呢?

    • **副作用(side effect)**其实本身是医学的一个概念,比如我们经常说吃什么药本来是为了治病,可能会产生一些其他的副作用;
    • 在计算机科学中,也引用了副作用的概念,表示在执行一个函数时,除了返回函数值之外,还对调用函数产生了附加的影响,比如修改了全局变量,修改参数或者改变外部的存储;
  • 纯函数在执行的过程中就是不能产生这样的副作用:

    • 副作用往往是产生bug的 “温床”。

# 4.3.纯函数的案例

  • 我们来看一个对数组操作的两个函数:

    • slice:slice截取数组时不会对原数组进行任何操作,而是生成一个新的数组;
    • splice:splice截取数组, 会返回一个新的数组, 也会对原数组进行修改;
  • slice就是一个纯函数,不会修改数组本身,而splice函数不是一个纯函数

var names1 = ['nba','abc','cba','ncaa','bba']
var names2 = ['nba','abc','cba','ncaa','bba']

var newNames1 = names1.slice(1,3)
console.log(names1)//['nba','abc','cba','ncaa','bba']//没有改变原数组
console.log(newNames1)//['abc','cba']

var newNames2 = names2.splice(2)
console.log(names2)//['nba','abc']//改变了原数组
console.log(newNames2)//['cba','ncaa','bba']
1
2
3
4
5
6
7
8
9
10

# 4.4.判断下面函数是否是纯函数?

//是纯函数
function add(num1,num2) {
  return num1 + num2
}

//不是纯函数(不满足:确定的输入,一定会产生确定的输出;)
var foo = 10
function add1(num) {
  return foo + num
}
console.log(add1(10))//20
foo = 20
console.log(add2(10))//30

//不是纯函数(不满足:函数在执行过程中,不能产生副作用;) 函数改变了外部的存储
var info = {
  name:'kobe'
}
function printInfo(obj) {
  console.log(obj)
  obj.name = 'james'
}
printInfo(info)//引用传递, 对传入的对象进行修改
console.log(info)//{name:'james'}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 4.5.纯函数的作用和优势

  • 为什么纯函数在函数式编程中非常重要呢?

    • 因为你可以安心的编写和安心的使用;
    • 你在写的时候保证了函数的纯度,只是单纯实现自己的业务逻辑即可,不需要关心传入的内容是如何获得的或者依赖其他的外部变量是否已经发生了修改;
    • 你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
  • React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:

    • React非常灵活,但它有一个严格的规则:所以React组件都必须像纯函数一样保护它们的props不被更改

# 5、柯里化

# 5.1.柯里化概念的理解

  • 柯里化也是属于函数式编程里面一个非常重要的概念。

    • 是一种关于函数的高阶技术;
    • 它不仅被用于 JavaScript,还被用于其他编程语言;
  • 我们先来看一下维基百科的解释:

    • 在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化;
    • 是把接收多个参数的函数,变成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数,而且返回结果的新函数的技术;
    • 柯里化声称 “如果你固定某些参数,你将得到接受余下参数的一个函数”;
  • 维基百科的结束非常的抽象,我们这里做一个总结:

    • 只传递给函数一部分参数 来调用它,让它返回一个函数去处理剩余的参数; 这个过程就称之为柯里化;
  • 柯里化是一种函数的转换,将一个函数从可调用的 f(a, b, c) 转换为可调用的 f(a)(b)(c)。

    • 柯里化不会调用函数。它只是对函数进行转换。

# 5.2.柯里化的代码转换

  • 那么柯里化到底是怎么样的表现呢?
// 1.未柯里化的函数
function add1(x, y, z) {
  return x + y + z
}
console.log(add1(1, 2, 3))//6

// 2.柯里化处理的函数
function add2(x) {
  return function (y) {
    return function (z) {
      return x + y + z
    }
  }
}
console.log(add2(1)(2)(3))//6

// 3.柯里化处理的函数箭头函数简写写法
// var add = x => {
//   return y => {
//     return z => {
//       return x + y + z
//     }
//   }
// }
var add3 = x => y => z => x + y + z

console.log(add3(1)(2)(3))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 5.3.柯里化优势一 - 函数的职责单一

  • 那么为什么需要有柯里化呢?

    • 在函数式编程中,我们其实往往希望一个函数处理的问题尽可能的单一,而不是将一大堆的处理过程交给一个函数来处理;
    • 那么我们是否就可以将每次传入的参数在单一的函数中进行处理,处理完后在下一个函数中再使用处理后的结果;
  • 比如上面的案例我们进行一个修改:传入的函数需要分别被进行如下处理

    • 第一个参数 + 2
    • 第二个参数 * 2
    • 第三个参数 ** 2
function add2(x) {
  x = x + 2
  return function (y) {
    y = y * 2
    return function (z) {
      z = z ** 2
      return x + y + z
    }
  }
}
console.log(add2(1)(2)(3))//16
1
2
3
4
5
6
7
8
9
10
11

# 5.4.柯里化优势二 - 函数的参数复用

  • 另外一个使用柯里化的场景是可以帮助我们可以复用参数逻辑:
    • createAdder函数要求我们传入一个count(并且如果我们需要的话,可以在这里对count进行一些修改);
    • 在之后使用返回的函数时,我们不需要再继续传入count了;
function createAdder(count) {
  return function (num) {
    return count + num
  }
}
var adder5 = createAdder(5)
adder5(10)
adder5(100)

var adder8 = createAdder(8)
adder8(2)
adder8(200)
1
2
3
4
5
6
7
8
9
10
11
12

# 5.5.柯里化案例练习

  • 这里我们在演示一个案例,需求是打印一些日志:

    • 日志包括时间、类型、信息;
  • 普通函数的实现方案如下:

function log(date,type,message) {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`)
}
log(new Date(),'DEBUG',"修复问题")
log(new Date(),'FEATURE',"新功能")
1
2
3
4
5
  • 柯里化函数的实现方案如下:
var log = date => type => message => {
  console.log(`[${date.getHours()}:${date.getMinutes()}] [${type}] [${message}]`)
}
var logNew = log(new Date())
logNew("DEBUG")("导航栏bug")
logNew("FEATURE")("添加新功能")

var logNewDebug = log(new Date())("DEBUG")
logNewDebug('点击无效bug')
logNewDebug('轮播图bug')
1
2
3
4
5
6
7
8
9
10

# 5.6.柯里化高级 - 自动柯里化函数

  • 目前我们有将多个普通的函数,转成柯里化函数
function hyCurrying(fn) {
  function curried(...args) {
    if(args.length >= fn.length) {
      return fn.apply(this,args)
    } else {
      return function(...args2) {
        return curried.apply(this,args.concat(args2))
      }
    }
  }
  return curried
}
function foo(n,m) {
  return n + m
}
var newCurried = hyCurrying(foo)
console.log(newCurried(500,20))//520
console.log(newCurried(500)(20))//520
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 6、组合函数

# 6.1.组合函数概念的理解

  • 组合(Compose)函数是在JavaScript开发过程中一种对函数的使用技巧、模式:
    • 比如我们现在需要对某一个数据进行函数的调用,执行两个函数fn1和fn2,这两个函数是依次执行的;
    • 那么如果每次我们都需要进行两个函数的调用,操作上就会显得重复;
    • 那么是否可以将这两个函数组合起来,自动依次调用呢?
    • 这个过程就是对函数的组合,我们称之为 组合函数(Compose Function);
    • 下面案例:实现将一个数 先做*2 再做平方
function double(num) {
  return num * 2 
}

function square(num) {
  return num ** 2
}
// 不用组合函数的方式
var doubleNum = double(3)
console.log(square(doubleNum))//36
var doubleNum1 = double(2)
console.log(square(doubleNum1))//16

// 用组合函数的方式
function compose(fn1,fn2,x) {
  return function(n) {
    return fn2(fn1(n))
  }
}
var calcFn = compose(double,square)
console.log(calcFn(3))//36
console.log(calcFn(2))//16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 6.2.实现组合函数

  • 刚才我们实现的compose函数比较简单

  • 我们需要考虑更加复杂的情况:比如传入了更多的函数,在调用compose函数时,传入了更多的参数

function compose(...fns) {
  var length = fns.length
  for(var fn of fns) {
    if(typeof fn !== 'function'){
      throw new TypeError('Expected a function')
    }
  }

  return function(...args) {
    var index = 0
    var result = length ? fns[index].apply(this,args) : args
    while(++index < length) {
      result = fns[index].call(this,result)
    }
    return result
  }
}

function add(n,m) {
  return n + m
}
function double(n) {
  return n * 2
}
function add520(n) {
  return n + 500
}
var newFn = compose(add,double,add520)
console.log(newFn(1,9))//520
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

# 7、with语句的使用

  • with语句 扩展一个语句的作用域链
var obj = {
  name:'hello world',
  age:20
}
with(obj) {
  console.log(name)//hello world
  console.log(age)//20
}
1
2
3
4
5
6
7
8
  • 不建议使用with语句,因为它可能是混淆错误和兼容性问题的根源。

# 8、eval函数

  • 内建函数 eval 允许执行一个代码字符串。

    • eval是一个特殊的函数,它可以将传入的字符串当做JavaScript代码来运行;
    • eval会将最后一句执行语句的结果,作为返回值;
  • 不建议在开发中使用eval:

    • eval代码的可读性非常的差(代码的可读性是高质量代码的重要原则);
    • eval是一个字符串,那么有可能在执行的过程中被刻意篡改,那么可能会造成被攻击的风险;
    • eval的执行必须经过JavaScript解释器,不能被JavaScript引擎优化;
var message = 'Hello World'
var evalString = `var age = 23;console.log(message);function foo() {return 50};foo();`
var result = eval(evalString)//23
console.log(result)//50
console.log(age)//23
console.log(message)//Hello World
1
2
3
4
5
6

# 9、严格模式

# 9.1.认识严格模式

  • JavaScript历史的局限性:

    • 长久以来,JavaScript 不断向前发展且并未带来任何兼容性问题;
    • 新的特性被加入,旧的功能也没有改变,这么做有利于兼容旧代码;
    • 但缺点是 JavaScript 创造者的任何错误或不完善的决定也将永远被保留在 JavaScript 语言中;
  • 在ECMAScript5标准中,JavaScript提出了严格模式的概念(Strict Mode)

    • 严格模式很好理解,是一种具有限制性的JavaScript模式,从而使代码隐式的脱离了 ”懒散(sloppy)模式“;
    • 支持严格模式的浏览器在检测到代码中有严格模式时,会以更加严格的方式对代码进行检测和执行;
  • 严格模式对正常的JavaScript语义进行了一些限制:

    • 严格模式通过 抛出错误 来消除一些原有的 静默(silent)错误;
    • 严格模式让JS引擎在执行代码时可以进行更多的优化(不需要对一些特殊的语法进行处理);
    • 严格模式禁用了在ECMAScript未来版本中可能会定义的一些语法;

# 9.2.开启严格模式

  • 那么如何开启严格模式呢?严格模式支持粒度话的迁移:
    • 可以支持在js文件中开启严格模式;
    • 也支持对某一个函数开启严格模式;
  • 严格模式通过在文件或者函数开头使用 use strict 来开启。
'use strict'
var name = 'kobe'
var obj = {
  name:'james',
  age:38
}
function foo() {
  'use strict'
   var message = "我是foo函数"
   return 10
}
foo()
1
2
3
4
5
6
7
8
9
10
11
12
  • 没有类似于 "no use strict" 这样的指令可以使程序返回默认模式。
    • 现代 JavaScript 支持 “class” 和 “module” ,它们会自动启用 use strict;

# 9.3.严格模式限制

  • 这里我们来说几个严格模式下的严格语法限制:

    • JavaScript被设计为新手开发者更容易上手,所以有时候本来错误语法,被认为也是可以正常被解析的;
    • 但是这种方式可能给带来留下来安全隐患;
    • 在严格模式下,这种失误就会被当做错误,以便可以快速的发现和修正;
  • 1. 无法意外的创建全局变量

'use strict'
function foo() {
  age = 18//意外的全局变量;此语句严格模式下不被允许
}
foo()
console.log(age)
1
2
3
4
5
6
  • 2. 严格模式会使引起静默失败(silently fail,注:不报错也没有任何效果)的赋值操作抛出异常
'use strict' 
var obj = {
  name:'kobe',

}

Object.defineProperty(obj,'name',{
  writable:false//设置obj的name属性为:不可写(即不可修改)
})
obj.name = "james"//上面已经将obj的name属性设置为不可写了(非严格模式下不报错也没有任何效果),但是此语句严格模式下不被允许
console.log(obj.name)
1
2
3
4
5
6
7
8
9
10
11
  • 3. 严格模式下试图删除不可删除的属性
'use strict' 
var obj = {
  name:'kobe',
}
Object.defineProperties(obj,{
  name: {
    configurable:false//设置为不可配置,即不可删除属性,不可配置属性的数据描述符中的(configurable/enumerable)和存取描述符(set()/get()),
  }
})

delete obj.name//上面已经将obj的name属性设置为不可配置了(非严格模式下不报错也没有任何效果),但是此语句严格模式下不被允许
console.log(obj.name)

1
2
3
4
5
6
7
8
9
10
11
12
13
  • 4.严格模式不允许函数参数有相同的名称
'use strict'
function foo(n,n) {//此语句严格模式下不被允许
  console.log(n)
}
foo(1,2)
1
2
3
4
5
  • 5.不允许0的八进制语法
'use strict'
console.log(0o123)
console.log(0123)//此语句严格模式下不被允许
1
2
3
  • 6. 在严格模式下,不允许使用with
  • 7.在严格模式下,eval不再为上层引用变量
'use strict'
var age = 20
var evalString = `console.log(age);var message = '6666'`
eval(evalString)//20
console.log(message)//此语句严格模式下不被允许
1
2
3
4
5
  • 8. 严格模式下,this绑定的原始数据类型不会默认转成对象(即包装类型)
  • 注意:严格模式下,独立函数调用this不指向window,而是undefined
'use strict'
function foo() {
  console.log(this)
}

foo.call("3")//3
foo.call(3)//3
foo.call(undefined)//undefined
foo.call(null)//null
foo()//undefined 
1
2
3
4
5
6
7
8
9
10

# 9.4.手写apply、call、bind函数实现(不考虑边界情况)

  • 接下来我们来实现一下apply、call、bind函数:

    注意:我们的实现是练习函数、this、调用关系,不会过度考虑一些边界情况

  • 实现apply、call方法

Function.prototype.hyApply = function(thisArg,args) {
  return this.hyExec(thisArg,args)
}
Function.prototype.hyCall = function(thisArg,...args) {
  return this.hyExec(thisArg,args)
}
Function.prototype.hyExec = function(thisArg,args) {
  thisArg  = thisArg ? Object(thisArg) : window
  thisArg.fn = this
  args  = args || []
  var result = thisArg.fn(...args)
  delete thisArg.fn
  return result
}


var obj = {name:'kobe'}
function foo(n,m) {
  console.log(this)
  return n + m
}
var n = foo.hyCall(1,1,2)
var m = foo.hyApply(obj,[2,3])

console.log(n,m)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
  • 实现bind方法
Function.prototype.hyBind = function (thisArg, ...argArray) {
  thisArg = thisArg ? Object(thisArg) : window
  thisArg.fn = this
  return function (...newArray) {
    var args = [...argArray, ...newArray]
    return thisArg.fn(...args)//会导致传进来的thisArg会多一个方法属性:fn
  }
}

var obj = { name: 'kobe' }
function foo(n, m, ...args) {
  console.log(this, args)//{name: "kobe", fn: ƒ} , (2) [520, 666]
  return n + m
}
var foo1 = foo.hyBind(obj, "13", "14")

console.log(foo1)
/* ƒ (...newArray) {
     var args = [...argArray, ...newArray]
     return thisArg.fn(...args)//会导致传进来的thisArg会多一个方法属性:fn
   }  */

var m = foo1(520, 666)
console.log(m)//1314
console.log(obj)//{name: "kobe", fn: ƒ}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 10、js对象的增强知识

# 10.1.对属性操作的控制

  • 在前面我们的属性都是直接定义在对象内部,或者直接添加到对象内部的:
    • 但是这样来做的时候我们就不能对这个属性进行一些限制:比如这个属性是否是可以通过delete删除的?这个属性是否在for in遍历的时候被遍历出来呢?
var obj = {
  name:'kobe',
  age:38,
  heigth:1.99
}
1
2
3
4
5
  • 如果我们想要对一个属性进行比较精准的操作控制,那么我们就可以使用属性描述符。
    • 通过属性描述符可以精准的添加或修改对象的属性;
    • 属性描述符需要使用 Object.defineProperty 来对属性进行添加或者修改;

# 10.2.Object.defineProperty

  • Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。Object.defineProperty(obj,prop,descriptor)

  • 可接收三个参数:

    • obj要定义属性的对象;
    • prop要定义或修改的属性的名称或 Symbol;
    • descriptor要定义或修改的属性描述符;
  • 返回值:传递给函数的对象。

# 10.3.属性描述符分类

  • 属性描述符的类型有两种:
    • 数据属性(Data Properties)描述符(Descriptor);
    • 存取属性(Accessor访问器 Properties)描述符(Descriptor);
  • 描述符可拥有的键值
    • 总结:如果一个描述符同时拥有 valuewritablegetset 键,则会产生一个异常。
configurable enumerable value writable get set
数据描述符 可以 可以 可以 可以 不可以 不可以
存取描述符 可以 可以 不可以 不可以 可以 可以

# 10.4.数据属性描述符

  • 数据数据描述符有如下四个特性:

  • [[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
  • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
  • [[Writable]]:表示是否可以修改属性的值;

    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Writable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Writable]]默认为false;
  • [[value]]:属性的value值,读取属性时会返回该值,修改属性时,会对其进行修改;

    • 默认情况下这个值是undefined;
var obj = {
    name:'kobe',
    age:23
}
obj.address = "北京"
//当我们直接在一个对象上定义某个属性或直接添加一个属性时
console.log(Object.getOwnPropertyDescriptor(obj,"name"))// {"value":"kobe","writable":true,"enumerable":true,"configurable":true}
console.log(Object.getOwnPropertyDescriptor(obj,"address"))// {"value":"北京","writable":true,"enumerable":true,"configurable":true}

// 当我们通过数据属性描述符定义一个属性时
Object.defineProperty(obj,'height',{})
console.log(Object.getOwnPropertyDescriptor(obj,"height"))// {"writable":false,"enumerable":false,"configurable":false}

console.log(Object.getOwnPropertyDescriptors(obj))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 数据属性描述符测试代码
var obj = {
    name:'kobe',
    age:39
}

Object.defineProperty(obj,'name',{
  enumerable:false,
})
console.log(Object.getOwnPropertyDescriptor(obj,'name'))// {"value":"kobe","writable":true,"enumerable":false,"configurable":true}
console.log(obj)//{"age":39}
//根据数据属性描述符定义属性
Object.defineProperty(obj,'address',{
//根据数据属性描述符定义属性时,默认值如下:
  //enumerable:false,
  //configurable:false,
  //writable:false,
  //value:undefined
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 10.5.存取属性描述符

  • 数据数据描述符有如下四个特性:

  • [[Configurable]]:表示属性是否可以通过delete删除属性,是否可以修改它的特性,或者是否可以将它修改为存取属性描述符;

    • 和数据属性描述符是一致的;
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Configurable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Configurable]]默认为false;
  • [[Enumerable]]:表示属性是否可以通过for-in或者Object.keys()返回该属性;

    • 和数据属性描述符是一致的;
    • 当我们直接在一个对象上定义某个属性时,这个属性的[[Enumerable]]为true;
    • 当我们通过属性描述符定义一个属性时,这个属性的[[Enumerable]]默认为false;
  • [[get]]:获取属性时会执行的函数。默认为undefined

  • [[set]]:设置属性时会执行的函数。默认为undefined

  • 存取属性描述符测试代码

var obj = {
    name:'kobe',
    age:39
}

var address = "北京市"

Object.defineProperty(obj,'address',{
//   configurable:true,
  enumerable:true,
  get() {
    return address
  },
  set(value) {
    address = value
  }
})

console.log(obj.address)//北京市
obj.address = "上海市"
console.log(obj.address)//上海市
console.log(obj)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 10.6.同时定义多个属性Object.defineProperties()

  • Object.defineProperties() 方法直接在一个对象上定义 多个 新的属性或修改现有属性,并且返回该对象。
  • Object.defineProperties(obj, props)
var obj = {
  _age:18
}

Object.defineProperties(obj, {
  name: {
    value:'kobe'
  },
  age: {
    enumerable:true,
    get() { 
      return this._age
    }
  }
})
console.log(obj)// {"_age":18,"age":18}
console.log(Object.getOwnPropertyDescriptors(obj))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

# 10.7.对象方法补充

  • 获取对象的属性描述符:
    • Object.getOwnPropertyDescriptor(obj,prop)
    • Object.getOwnPropertyDescriptors(obj)
var obj = {
  name:'kobe',
  age:38
}
Object.defineProperty(obj,'address',{})
console.log(Object.getOwnPropertyDescriptor(obj,'age'))// {"value":38,"writable":true,"enumerable":true,"configurable":true}
console.log(Object.getOwnPropertyDescriptor(obj,'address'))//{"writable":false,"enumerable":false,"configurable":false}
console.log(Object.getOwnPropertyDescriptors(obj))//获取obj对象的所有属性的描述符
1
2
3
4
5
6
7
8
  • 禁止对象扩展新属性:Object.preventExtensions(obj)
    • 给一个对象添加新的属性会失败(在严格模式下会报错);
'use strict'
var obj = {
  name:'kobe',
  age:38
}
Object.preventExtensions(obj)
obj.address = '北京'//严格模式下报错:Cannot add property address, object is not extensible(无法添加属性地址,对象不可扩展)
1
2
3
4
5
6
7
  • 密封对象,不允许配置和删除属性:Object.seal(obj)

    • 实际是调用Object.preventExtensions(obj)
    • 并且将现有属性的configurable:false
  • 冻结对象,不允许修改现有属性: Object.freeze(obj)

    • 实际上是调用Object.seal(obj)
    • 并且将现有属性的writable: false
最后更新时间: 2022/07/31, 21:14:02
彩虹
周杰伦