影子的知识库

影子的知识库

  • 知识库
  • GitHub

›JS 基础

JVM系列

  • JVM内存区域
  • 对象创建-布局-访问
  • 内存溢出实战
  • 内存区域回收
  • 四大引用
  • 垃圾回收算法
  • HotSpot回收算法细节

Java系列

  • java注解
  • springboot请求参数绑定
  • springboot请求参数校验框架
  • YAML语法
  • 动态代理
  • classpath和java命令
  • springboot-aop编程
  • springboot统一异常处理
  • springboot数据库和事务
  • springboot拦截器
  • springboot中的web配置
  • docker的简单开发
  • springboot自动配置
  • 数据库的隔离级别
  • springboot监控
  • java类加载
  • java-agent的相关内容
  • 类加载器详解
  • java的SecurityManager
  • maven学习

Node

    JS 基础

    • 语法基础和数据类型
    • 数据类型转换
    • 语句 表达式 运算符
    • 变量与对象
    • 函数
    • 数据处理
    • 常用 API
    • 重点知识

    ES6

    • 块级作用域
    • 字符串和正则表达式
    • 函数
    • 对象
    • Symbol
    • Set和Map
    • 迭代器和生成器
    • 类
    • 数组
    • Promise

    Node 基础

    • 模块系统
    • package.json
    • 内置对象
    • npm脚本的使用
    • Buffer
    • Stream
    • 事件循环机制
    • 示例代码

    stream系列

    • 流的缓冲
    • 可读流
    • 可写流
    • 双工流和转换流
    • 自定义流

后期计划

  • 学习计划
  • 专题研究计划
Edit

本文内容

本文主要是记录一些重点知识,包括原型链、NEW 运算符,this 引用等

原型链

记住以下 2 条即可:

  • JavaScript 对象有一个指向一个原型对象的引用(称之为 __proto__)。当试图访问一个对象的属性时,它不仅仅在该对象上搜寻,还会搜寻该对象的原型对象,以及该对象的原型对象的原型对象,依次层层向上搜索,直到找到一个名字匹配的属性或到达原型链的末尾(一直找到 Object 到 null)。例如:

    var obj1 = {
        x: 1,
        name: function () {
            return 'obj1';
        }
    };
    var obj2 = {
        y: 2,
        name: function () {
            return 'obj2';
        }
    };
    var obj3 = {
        z: 3,
        name: function () {
            return 'obj3';
        },
        toString: function () {
            for (var x in this) {
                if (x !== 'name' && x !== 'toString') {
                    console.log('Object: ' + this.name() + ', ' + x + ': ' + this[x]);
                }
            }
        }
    };
    obj1.__proto__ = obj2;
    obj2.__proto__ = obj3;
    // 经过以上操作,现在 obj1 的原型对象是 obj2,obj2 的原型对象是 obj3
    // 因此 obj1 拥有 obj2 和 obj3 的属性 y 和 z,obj2 拥有 obj3 的属性 z
    obj1.toString();
    obj2.toString();
    obj3.toString();
    // 输出:
    // Object: obj1, x: 1
    // Object: obj1, y: 2
    // Object: obj1, z: 3
    // Object: obj2, y: 2
    // Object: obj2, z: 3
    // Object: obj3, z: 3
    

    注意:

    __proto__ 不是一个标准属性,但是许多 JS 环境都实现了该属性。

    从 ES 6 开始,我们可以通过 Object.getPrototypeOf(obj) 和 Object.setPrototypeOf(obj) 来访问一个对象的原型对象,这个等同于非标准实现的对象的 __proto__ 属性。

    为了避免混淆,本文就一直使用 __proto__ 来代表一个对象的原型对象。

    千万注意 __proto__ 和下文要讲的 prototype 属性是两个完全不同的属性

  • 被构造函数创建的实例对象的 ___proto__ 指向这个构造函数的 prototype 属性,例如:

    function a() { }
    /**
     * a 对象(函数也是个对象)的原型现在是这个 { x: 3 } 对象
     * 因此 a.x = 3
     */
    a.__proto__ = { x: 3 };
    
    a.prototype.x = 5; // 将 a 的 prototype 属性对象,添加一个属性 x = 5
    
    /**
     * 由构造函数(a 对象)生成的对象,它的原型对象等于构造函数对象的 prototype 属性
     * 也就是说 b.__proto__ === a.prototype 此时是成立的
     * 如果我调用 a.prototype = otherObj,直接修改指针指向的话
     * 那么 b.__proto__ === a.prototype 这个等式就不成立了
     */
    var b = new a();
    
    console.log(a.x); // a.x = a.__proto__.x = 3
    /**
     * b.x = b.__proto__.x = a.prototype.x = 5
     * 记住(b.__proto__ === a.prototype)
     */
    console.log(b.x); 
    

    也可以这么说:只有构造函数对象的 prototype 属性才是有意义的,普通对象的 prototype 属性没有其它的含义(暂时理解是这样),而所有对象(包括构造函数对象)的 ___proto__ 属性都指向了这个对象本身的原型对象。

this 引用规则

最外层代码中,this 引用的是全局对象(非模块化的情况下,在 node 中无法还原 this === global,在浏览器环境中有 this === window)

在函数内,this 引用根据 函数调用方式 具有不同的指向:

函数的调用方式this 引用的引用对象
构造函数调用构造的新对象
方法调用接收方对象,例如 obj.method()指向 obj
apply 或是 call 调用由 apply 或 call 的参数指定的对象
其他方式的调用全局对象
触发事件时的回调函数谁触发的事件,this 就指向谁

接收方对象的示例:

var obj = {
    x: 100,
    doit: function () {
        console.log('method is called.' + this.x);
        var innerObj = {
            x: 20,
            inner: function () {
                console.log('inner method is called.' + this.x);
            }
        }
        innerObj.inner();
    }
}
obj.doit();
// 输出:
// method is called.100
// inner method is called.20

进一步说明接收方的概念(以下示例是跑在 node 环境中,所以全局对象是 global):

global.x = 3; // 给全局对象添加属性 x = 3
var obj = {
    x: 100,
    doit: function () {
        console.log('method is called.' + this.x);
    }
}
obj.doit(); // 接收方是 obj,所以输出的是 obj.x = 100
var tmp = obj.doit;
tmp(); // 没有接收方,this 指向全局对象 global,所以输出的是 global.x = 3;

也就是说形如 obj.method() 的调用方式,接收方就是 obj

apply 与 call

通过 apply 与 call 调用的函数的 this 引用可以指向任意对象。也就是说,它们可以显式地指定接收对象,例如:

function f() {
    console.log(this.x);
}
var obj = { x: 4 };
f.apply(obj); // 指定此次调用中,f指向的接收对象是 obj,因此输出 obj.x = 4
f.call(obj); // 指定此次调用中,f指向的接收对象是 obj,因此输出 obj.x = 4
var obj = {
    x: 3,
    doit: function () {
        console.log('method is called.' + this.x);
    }
};
var obj2 = { x: 5 };
obj.doit.apply(obj2); // 指定此次调用中,f指向的接收对象是 obj2,因此输出 obj2.x = 5

两种方法的第一个参数都是接收方对象,两种方式的区别仅仅在于后续传递参数的方式,例如:

function f(a, b) {
    console.log('this.x = ' + this.x + ', a = ' + a + ', b = ' + b);
}
f.apply({ x: 4 }, [1, 2]); // apply 后续参数需要使用数组传递 [1,2]
f.call({ x: 4 }, 1, 2); // call 后续参数按照原来参数的顺序进行传递 1,2

NEW 运算符

MDN 文档:NEW 运算符

new 运算符会进行如下操作:

  1. 创建一个空的简单 JavaScript 对象,也就是 {},假设起一个临时名字是 tmp
  2. 将新对象 tmp 的原型对象指向构造函数(假设是 Func() 函数)的 prototype 属性,也就是说 tmp.__proto__ = Func.prototype
  3. 此时 tmp.constructor = Func.prototype.constructor = Func
  4. 步骤 3 有两个关键,一个是 tmp.constructor = Func,一个是 Func.prototype.constructor = Func
    • 也就是说新对象的 constructor是从原型链继承来的
    • 函数对象的 prototype 属性上,有一个属性 prototype.constructor 指向函数对象自身
  5. 调用新对象的 constructor 方法,也就是 tmp.constructor(...)
  6. 步骤 5 的接收对象是 tmp,因此构造函数中 this 指向 tmp
  7. 如果 tmp.constructor(...) 没有返回新对象,则返回 this,也就是 tmp 对象
  8. new Func 等同于 new Func(),也就是进行没有任何参数的构造函数调用

示例:

function a(arg) { this.x = 1; console.log(arg); }
var b = new a(); // 输出 undefined

/**
 * b.constructor = b.__proto__.constructor 且 b.__proto__ = a.prototype
 * 因此 b.__proto__.constructor = a.prototype.constructor
 * 而 a.prototype.constructor = a 自身
 * 所以 b.constructor = a
 * 这里输出 [Function: a]
 */
console.log(b.constructor); 

b.__proto__ = Object.prototype;

/**
 * 此时 b.constructor = b.__proto__.constructor = Object.prototype.constructor
 * 因此 b.constructor = Object
 * 所以输出 [Function: Object]
 */
console.log(b.constructor);

a.prototype.constructor(11111); // 等价于 a(11111)调用,因此输出 11111

元属性(属性描述符)

从 ES 5 开始,所有属性都有属性描述符,参考下面的代码:

var myObject = { a: 2 };
console.log(Object.getOwnPropertyDescriptor(myObject, "a"));

它的输出是:

{ value: 2, writable: true, enumerable: true, configurable: true }

这就是 myObject.a 这个属性的描述符,也就是描述这个属性的属性,我将它称为元属性。(所谓元属性就是描述一个属性本身应该有哪些属性,例如我们的数据库,数据库里存的记录是数据,而描述这些记录的记录是元数据,例如数据库表的定义,描述一个表应该有哪些字段,字段应该是什么类型,这就是元数据)

在创建普通属性时,属性描述符会使用默认值,我们也可以用 Object.defineProperty(..) 来添加一个新属性或者修改已有属性(如果它是 configurable),例如:

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: true,
    configurable: true,
    enumerable: true
});
console.log(myObject.a); // 2

在 ES 标准中,属性有 2 种类型,一种是数据属性,一种是访问器属性

  • 数据属性的属性描述符包含有 4 种属性,分别是 value、writable、configurable、enumerable
  • 访问器属性的属性描述符包含有 4 种属性,分别是 configurable、enumerable、get、set

数据属性

Writable

Writable 决定是否可以修改属性的值,例如:

var myObject = {};
Object.defineProperty(myObject, "a", {
    value: 2,
    writable: false, // 不可写! 
    configurable: true, enumerable: true
});
myObject.a = 3; // 这里修改 myObject.a 将会失败
console.log(myObject.a); // 2

在严格模式("use strict";)下,甚至会报错。说白了这个元属性类似于 java 的 final 修饰符

默认值:true

Configurable

Configurable 决定我们是否可以用 defineProperty(..) 方法来修改元属性,例如:

var myObject = { a: 2 };
myObject.a = 3;
console.log(myObject.a); // 3
Object.defineProperty(myObject, "a", {
    value: 4,
    writable: true,
    configurable: false, // 不可配置!
    enumerable: true
});
console.log(myObject.a); // 4 
myObject.a = 5;
console.log(myObject.a); // 5
Object.defineProperty(myObject, "a", {
    value: 6,
    writable: true,
    configurable: true,
    enumerable: true
}); // TypeError

不管是不是处于严格模式,尝试修改一个不可配置的属性描述符都会出错。这个元属性决定了我们是否能继续修改这些元属性,所以一旦为 false,就永远不能撤销了

除了无法修改元属性之外,这个属性本身也无法被删除了,例如:

var myObject = { a: 2 };
console.log(myObject.a); // 2
delete myObject.a;
console.log(myObject.a); // undefined
Object.defineProperty(myObject, "a", {
    value: 5,
    writable: true,
    configurable: false,
    enumerable: true
});
console.log(myObject.a); // 5 
delete myObject.a; // 静默失败
console.log(myObject.a); // 5

默认值:true

Enumerable

这个描述符控制的是属性是否会出现在对象的属性枚举中, 比如说 for..in 循环。 如果把 enumerable 设置成 false , 这个属性就不会出现在枚举中。

默认值:true

Value

包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候, 把新值保存在这个位置。

从上文可以看出,这个元属性代表了属性的值。

默认值:undefined

访问器属性

访问器属性不包含数据值;它们包含一对儿 getter 和 setter 函数(不过,这两个函数都不是必需的)。 在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据。访问器属性有如下 4 个特性。

configurable

同数据属性的 configurable 元属性

enumerable

同数据属性的 enumerable 元属性

get (ES 6 之后的语法)

在读取数据值时会被调用的函数,类似于 java 里的 getter 方法,只不过 JS 里定义 get 之后,我们可以直接用属性名称来代替 get 方法的调用,例如:

var obj = {
    log: ['a', 'b', 'c'],
    get latest() {
        if (this.log.length == 0) {
            return undefined;
        }
        return this.log[this.log.length - 1];
    }
}

console.log(obj.latest);
  // expected output: "c"

如果在 ES 6 之前的话,我们只能使用 Object.defineProperty 来定义访问器属性

访问器属性 get 注意以下问题

  • 可以使用数值或字符串作为标识,也就是例如 latest 方法,可以改成 get 1(),然后使用 obj[1] 进行访问这个属性
  • 必须不带参数
  • 相同属性不能定义多个 get 方法,而且如果该属性有 value 了,不能定义 get 方法
  • 当只是指定了 get,没有指定 set 的时候,表示这个值是只写的,尝试写入会静默失败,在严格模式下会报错

作用和技巧:

  • 当属性计算比较复杂时,用到的时候才计算这个属性

  • 当属性计算很耗费资源的时候,我们可以先定义 get 方法,使用一次去获取属性值之后,再将其删除,然后添加成数据属性,例如:

    get notifier() {
      delete this.notifier;
      return this.notifier = document.getElementById('bookmarked-notification-anchor');
    },
    // 上面先是定义成 get,然后再方法里就将其定义成了数据属性,这样可以延迟计算
    

set

当尝试设置属性时,set语法将对象属性绑定到要调用的函数,例如:

var language = {
    set current(name) {
        this.log.push(name);
    },
    log: []
}

language.current = 'EN';
language.current = 'FA';

console.log(language.log);
  // expected output: Array ["EN", "FA"]

说白了还是类似于 java 的 get/set 机制。明显来说,当我们定义了一对 get/set 的时候,将相当于定义了一个伪属性,该属性可以读写,跟数据属性很相似。

使用 set 时需要注意以下问题:

  • 可以使用数值或字符串作为标识,同上方列举的 get 的注意点的第一条
  • 它必须有一个明确的参数,实际上就是我们赋值的那个值会作为参数
  • 不能为一个已有真实值的变量使用 set,也不能为同一个属性设置多个 set(稍微思考下就知道这个是必然的)
Last updated on 11/8/2020
← 常用 API块级作用域 →
  • 原型链
  • this 引用规则
  • apply 与 call
  • NEW 运算符
  • 元属性(属性描述符)
    • 数据属性
    • 访问器属性
影子的知识库
Docs
Getting Started (or other categories)Guides (or other categories)API Reference (or other categories)
Community
User ShowcaseStack OverflowProject ChatTwitter
More
BlogGitHub
Copyright © 2020 Cen ZhiPeng