JavaScript中的代码复用——this、对象、类(1)对象与this

前言

一共有三篇,这一篇总结复习一下基础知识。包括js中的作用域链和原型链、this的指向、对象的一些特性与方法。
内容来源于《你不知道的js》《阮一峰ES6入门》《JavaScript语言精粹》《JavaScript高级程序设计》《JavaScript设计模式》《JavaScript模式》《MDN web文档》
本博客没有什么有价值的知识,仅作总结梳理之用,初学者可以看看。

起步

编辑器:vscode
插件:Quokka

演示

可以在语句后直接出结果不需要运行或者log。所以代码都用return来返回结果

对象

属性

虽然在代码中常用foo.a来访问属性值,但是还有[]这种方式,计算属性名或Symbol属性名都要用这种方式访问

let a = 'a'
let s1 = Symbol('c')
let myObj = {
  [a+'b']: 'Hello',
  [s1]: 'World'
}
myObj['ab'] //? Hello
myObj[s1] //? World
Reflect.ownKeys(myObj) //?['ab',Symbol(c)]

函数对象

JS中函数是对象的子类型,区别在于函数可以被调用。因为函数是对象,所以函数可以像其他任何值一样被使用,可以保存在对象、变量中,可以被当作参数传给其他函数。可以拥有自己的属性,并且属性也可以是函数,可以函数调用函数。
函数被创建时有几个自带的属性,值得注意的是prototype属性每个函数都有这个属性即使没有将此函数进行过new构造调用。

function foo() {
  return 2
}
Object.getOwnPropertyDescriptors(foo) 
​​​​​{ length: ​​​​​
​​​​​   { value: 1,​​​​​
​​​​​     writable: false,​​​​​
​​​​​     enumerable: false,​​​​​
​​​​​     configurable: true },​​​​​
​​​​​  name: ​​​​​
​​​​​   { value: 'foo',​​​​​
​​​​​     writable: false,​​​​​
​​​​​     enumerable: false,​​​​​
​​​​​     configurable: true },​​​​​
​​​​​  arguments: ​​​​​
​​​​​   { value: null,​​​​​
​​​​​     writable: false,​​​​​
​​​​​     enumerable: false,​​​​​
​​​​​     configurable: false },​​​​​
​​​​​  caller: ​​​​​
​​​​​   { value: null,​​​​​
​​​​​     writable: false,​​​​​
​​​​​     enumerable: false,​​​​​
​​​​​     configurable: false },​​​​​
​​​​​  prototype: ​​​​​
​​​​​   { value: foo {},​​​​​
​​​​​     writable: true,​​​​​
​​​​​     enumerable: false,​​​​​
​​​​​     configurable: false } }​​​​​

方法与引用类型

当一个对象的属性是一个函数的时候,普遍称之为对象的方法。当调用这个函数时就是调用了这个对象的方法。实际来看,函数并不真的属于某个对象,只是对象有一个指向某个函数对象的指针。
同样的在对象的属性中,保存对象、数组等和函数一样都是保存的引用。
这就导致随便设一个变量取得指向此属性值的指针,它修改后的结果都会影响原对象。因为共用的一个值,当复制对象的时候如果简单的复制下属性,也会导致新对象与原对象共用一个值,两者互相影响。

let foo = {
  a: [1,2,3]
}
let bar = foo.a
bar.push(4)
foo.a //? [1,2,3,4]

属性描述符

从上文获取函数属性时可以看出,属性有一些自己的性质。所有的属性都用 属性描述符(Property Descriptors) 来描述,譬如foo.a,因为它仅持有一个数据值所以又可以称为“数据描述符”。下面的属性可以用Object.defineProperty来进行设置

  • 可写性(Writable)控制改变属性的能力
  • 可配置性(Configurable)控制是否可以配置设置为false则是不可以的单向操作
  • 可枚举型(Enumerable)控制着一个属性是否能在特定的对象属性枚举操作中出现
    当设置为false即使可以访问也不会被枚举

Getter和Setter

foo.a属性访问的时候实际上是在对象上进行了[[Get]]操作,如果没有找到对象属性就会沿着原型链向上寻找,什么都没找到会返回undefined,同理也有个用来设置或创建属性的操作。JS中用来操作这两个行为的函数称之为取值函数(getter)和存值函数(setter)
当将一个属性定义为拥有getter或setter,那么它的定义就成为了“访问器描述符”(与“数据描述符”相对),

let foo = {
  get a() {
    return this.b
  },
  set a(arg) {
    this.b = arg*2
  }
}
foo.a = 2
foo.b //? 4
foo.a //? 4

枚举与迭代

  • Object.keys() 返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含Symbol属性)
  • for..in.. 循环遍历对象自身的和继承的可枚举属性(不含Symbol属性)
  • Object.getOwnPropertyNames(obj) 返回一个数组,包含对象自身的所有属性(不含Symbol属性,但是包括不可枚举属性).
  • Reflect.ownKeys(obj) 返回一个数组,包含对象自身的所有属性,不管属性名是Symbol或字符串,也不管是否可枚举.

一些方法

罗列一下后面要用到的

Object.create(proto, [propertiesObject]) 以proto为原型创建个新对象并加入到原型链中去
Object.getPrototypeOf() 返回指定对象的原型(内部_Prototype_属性的值)。
Object.getOwnPropertyDescriptors(obj) 用来获取一个对象的所有自身属性的描述符。
Object.hasOwnProperty() 方法会返回一个布尔值,指示对象自身属性中是否具有指定的属性
Object.isPrototypeOf() 方法用于测试一个对象是否存在于另一个对象的原型链上。
Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即内部_Prototype_属性)到另一个对象因为性能问题不要直接设置而是用create创建。
for...in 语句以任意顺序遍历一个对象的可枚举属性。对于每个不同的属性,语句都会被执行。
Object.entries()返回一个数组,其元素是与直接在object上找到的可枚举属性键值对相对应的数组。属性的顺序与通过手动循环对象的属性值所给出的顺序相同。

作用域与闭包

作用域

作用域控制着变量与参数的可见性和生命周期。
JS中的作用域可以理解成词法作用域 就是在词法分析时被定义的作用域。换句话说,也就是在写代码时,变量写在哪,作用域就由此决定。

function foo(a) {
    let b = a * 2;
    function bar(c) {
        console.log( a, b, c );
    }
    bar(b * 3);
}
foo( 2 ); // 2 4 12

上述代码作用域有三层,最内层bar中只包含c但是却可以访问上层的b。依靠的是在JS存在的作用域链,当寻找标识符时,在作用域链的规则下,从里向外寻找,找到第一个匹配停止,所以导致了内层访问外层和内层对外层同名标识符造成遮蔽。

闭包

闭包:函数可以访问它被创建时所处的上下文环境,被称为闭包。
闭包:闭包是指有权访问另一个函数作用域中变量的函数。
闭包:函数依然有对创建时环境的引用,这个引用称之为闭包。
三种闭包的定义,让人分不清闭包是函数、引用还是行为?但是意思都差不多

let fn
function foo() {
    let a = 2
    function baz() {
        console.log( a )
    }
    fn = baz
}
foo()
fn() //2

可以看到fn保留了baz创建时的作用域,假设分为三层,fn1、foo2、baz3。那过程就是在1层的fn通过在3层的baz访问了在2层的a。

this指向

this的作用是传递对象引用,也就是改变函数运行的上下文(context)。
前面写在JS中的作用域是词法作用域,但是this却和动态作用域类似

let obj = {
    id: "obj",
    sayId() {
    console.log(this.id)
  }
}
let id = "global"
obj.sayId() //obj
===================
let obj = {
    id: "obj",
    sayId() {
    console.log(id)
  }
}
let id = "global"
obj.sayId() //global
当没用this时,就是普通的运行函数,即使函数是某个对象的方法,作用域也是在书写时就确认了的。
这个函数在全局作用域中运行,取得的值也是全局作用域的。
但如果用了this那就会取调用点的值。

丢失this

也就是说代码没能如自己写的那样this指向。为什么会出现下面的情况呢。因为在传递函数参数时,发生了隐含的引用赋值

let obj = {
    id: "obj",
    sayId() {
    console.log(this.id)
  }
}
let id = "global"
obj.sayId() // obj
setTimeout( obj.sayId, 100 ) //undefined
=============
let bar = obj.sayId
bar() //? undefined
传参时就发生了这样的引用函数没有被对象调用而是在全局作用域中运行。所以this丢失

五种绑定

  • 隐含绑定
    谁调用this就指向谁。还有一种默认绑定在全局作用域调用会指向全局对象的,但是在严格模式下会返回undefined的。所以不归类。
function foo() {
    console.log( this.a );
}
let obj = {
    a: 2,
    foo: foo
}
obj.foo(); // 2
  • 明确绑定
    利用call() apply() bind()来强制this指向。
function foo() {
    console.log( this.a );
}
let obj = {
    a: 2
};
foo.call( obj ); // 2
===========================
function foo(something) {
    console.log( this.a, something )
    return this.a + something
}
let obj = {
    a: 2
}
let bar = foo.bind( obj )
let b = bar( 3 ) // 2 3
console.log( b );// 5
bind(..)返回一个硬编码的新函数,它使用你指定的this环境来调用原本的函数。
  • new绑定
    this指向new构造调用函数而返回的新对象
function foo(a) {
    this.a = a
}
let bar = new foo( 2 )
console.log( bar.a ) // 2
  • super绑定
    这个指向当前对象的原型对象,也就是_proro_链上一层对象。
    下面代码可以这样理解super.a()等于foo.a.call(this)super.x等于foo.x
let foo = {
  x: 1,
  a() {
    return this.x
  }
}
let bar = {
  x: 2,
  b() {
    return `super.a()结果是${super.a()},super.x结果是${super.x}`
  }
}
Object.setPrototypeOf(bar,foo)
bar.b() //? ​​​​​​​​​​​​​​​super.a()结果是2,super.x结果是1​​​​​
  • 箭头函数
    箭头函数没有自己的this,只会从自己的作用域链的上一层继承this,并且固化无法再更改。也就说,this遵循的是词法作用域规则,而不是调用点绑定规则。
let foo = {
  x:1,
  a(){
    return () => this.x //直接写无法定义,只能用返回值的方法。
  },
  b() {
    return this.x
  }
}
let bar = {
  x: 2
}
let c = foo.a()
c() //? 1
foo.b() //? 1
foo.b.call(bar) //? 2
c.call(bar) //? 1无法改变

总结一下就是,this的调用取决于调用点,call等可以强制改变this,箭头函数会固化词法作用域this,new会指向创建的新对象,super则指向对象的原型对象。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,519评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,842评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,544评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,742评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,646评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,027评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,513评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,169评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,324评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,268评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,299评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,996评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,591评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,667评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,911评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,288评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,871评论 2 341

推荐阅读更多精彩内容

  • 函数和对象 1、函数 1.1 函数概述 函数对于任何一门语言来说都是核心的概念。通过函数可以封装任意多条语句,而且...
    道无虚阅读 4,511评论 0 5
  • 第3章 基本概念 3.1 语法 3.2 关键字和保留字 3.3 变量 3.4 数据类型 5种简单数据类型:Unde...
    RickCole阅读 5,092评论 0 21
  • 这是16年5月份编辑的一份比较杂乱适合自己观看的学习记录文档,今天18年5月份再次想写文章,发现简书还为我保存起的...
    Jenaral阅读 2,718评论 2 9
  • 今天正在吃午饭的时候,当当送书的快递员抱了一箱书给我!,Aimy汤还没喝完,迫不及待的打开箱子,这时我也凑过去拿起...
    06211阅读 1,297评论 0 0
  • 有两端( C / S ),一端是用户应用(C),获取授权码(以下简称 token);另一端是授权中心(S),给用户...
    杰哥长得帅阅读 609评论 0 0