ECMAScript学习笔记——Symbol

//1.概述

//ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

//ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

//Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

let s = Symbol(); //语法,生成一个symbol类型的值s。不能用new不是对象

typeof s; // "symbol"
//上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。
//注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

//Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo'); //传进来的参数表示对这个值的描述。比如s1的描述是账号,s2的描述是密码,为了区分用的
let s2 = Symbol('bar');

s1; // Symbol(foo)
s2; // Symbol(bar)

s1.toString(); // "Symbol(foo)"
s2.toString(); // "Symbol(bar)"
//上面代码中,s1和s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

//如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。
const obj = {
    toString() {
        return 'abc';
    }
};
const sym = Symbol(obj);
sym; // Symbol(abc)     调用了toString方法

//注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
// 没有参数的情况
let s1_1 = Symbol();
let s1_2 = Symbol();

s1_1 === s1_2; // false

// 有参数的情况
let s1_3 = Symbol('foo');
let s1_4 = Symbol('foo');

s1_3 === s1_4; // false
//上面代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。

//Symbol 值不能与其他类型的值进行运算,会报错。
//let sym = Symbol('My symbol');

//"your symbol is " + sym;
// TypeError: can't convert symbol to string
//`your symbol is ${sym}`;
// TypeError: can't convert symbol to string

//但是,Symbol 值可以显式转为字符串。
let sym1_5 = Symbol('My symbol');

String(sym1_5); // 'Symbol(My symbol)'      传入参数转为字符串类型
sym1_5.toString(); // 'Symbol(My symbol)'

//另外,Symbol 值也可以转为布尔值,但是不能转为数值。

let sym1_6 = Symbol();
Boolean(sym1_6); // true
!sym1_6; // false

if (sym1_6) {
    console.log(1);
}

//Number(sym1_6); // TypeError
//sym1_6 + 2; // TypeError


//2、作为属性名的 Symbol
//由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。
let mySymbol = Symbol();

// 第一种写法
let a2_2 = {};
a2_2[mySymbol] = 'Hello!';

// 第二种写法
let a2_3 = {
    [mySymbol]: 'Hello!'
};

// 第三种写法
let a2_4 = {};
Object.defineProperty(a2_4, mySymbol, { value: 'Hello!' }); //在对象上定义或修改属性,第一个参数是对象,第二个参数是属性的描述符,第三个参数的属性的描述符被定义或修改(也就是descriptor,描述属性的值和是否可遍历修改等,这里只写了value,别的为默认)。

// 以上写法都得到同样结果
a2_2[mySymbol]; // "Hello!"
//上面代码通过方括号结构和Object.defineProperty,将对象的属性名指定为一个 Symbol 值。

//注意,Symbol 值作为对象属性名时,不能用点运算符。
const mySymbol2_5 = Symbol();
const a2_5 = {};

a2_5.mySymbol2_5 = 'Hello!'; //因为用点运算符以后,后面跟的都是字符串,所以只是相当于定义了一个mySymbol2_5属性,并不是Symbol类型,所以给对象声明一个Symbol类型的属性的写法只有上面三种写法
a2_5[mySymbol2_5]; // undefined
a2_5['mySymbol2_5']; // "Hello!"

//上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。

//同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let s2_6 = Symbol();

let obj2_6 = {
    [s2_6]: function(arg) { console.log() }
};

obj2_6[s2_6](123);
//上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个 Symbol 值。
//采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。
let obj2_7 = {
    [s2_6](arg) { console.log() } //es6允许省略function
};

//Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
let log = function(params) {
    let a = 2;
    //this.name = "一个函数表达式的构造函数";     //写的this只有实例化之后才会有用,传递给实例化的对象
};
log.levels = {
    DEBUG: Symbol('debug'),
    INFO: Symbol('info'),
    WARN: Symbol('warn')
};
log(log.levels.DEBUG, 'debug message'); //不太清楚这么写的含义,应该是第一个参数是要修改的键第二个是要修改的值,这样传入进去之后就算相同的值也不会相等,因为是Symbol类型,唯一的。
log(log.levels.INFO, 'info message');

//下面是另外一个例子。

const COLOR_RED = Symbol();
const COLOR_GREEN = Symbol();

function getComplement(color) {
    switch (color) {
        case COLOR_RED:
            return COLOR_GREEN;
        case COLOR_GREEN:
            return COLOR_RED;
        default:
            throw new Error('Undefined color'); //不是红绿色就抛出错误,这俩都是Symbol类型的唯一值,所以不会发生意外。
    }
}
//常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。

//还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。


//3、实例:消除魔术字符串      
//魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
function getArea(shape, options) {
    let area = 0;

    switch (shape) {
        case 'Triangle': // 魔术字符串
            area = .5 * options.width * options.height;
            break;
            /* ... more code ... */
    }

    return area;
}

getArea('Triangle', { width: 100, height: 100 }); // 魔术字符串
//上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

//常用的消除魔术字符串的方法,就是把它写成一个变量。
const shapeType3_1 = {
    triangle: 'Triangle'
};
//解耦:变量和方法都分开写,只有调用的时候才组织到一起,这样才算解耦,比如方法b要用方法a的返回值,可以声明变量c=a(i);拿到返回值以后再调用b(c);这样就可以了。
function getArea3_1(shape, options) {
    let area = 0;
    switch (shape) {
        case shapeType3_1.triangle:
            area = .5 * options.width * options.height;
            break;
        case shapeType3_2:
            area = "Symbol值";
            break;
    }
    return area;
}

getArea3_1(shapeType3_1.triangle, { width: 100, height: 100 });
//上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

//如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。
const shapeType3_2 = {
    triangle: Symbol() //这样switch语句执行判断的时候根据传进来的值做了判断,只要这个值是唯一的不影响判断就可以根据传进来的值执行相应的代码。
};
//上面代码中,除了将shapeType.triangle的值设为一个 Symbol,其他地方都不用修改。


//4、属性名的遍历
//Symbol 作为属性名,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。

//Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj4_1 = {};
let a4_1 = Symbol('a');
let b4_1 = Symbol('b');

obj4_1[a4_1] = 'Hello'; //obj4_1的两Symbol属性,a和b是描述符,可以理解为说明或备注,两个Symbol属性的值分别是hello和world。
obj4_1[b4_1] = 'World';

const objectSymbols4_1 = Object.getOwnPropertySymbols(obj4_1); //返回所有Symbol属性,注,并不会返回Symbol属性的值。

objectSymbols4_1; // [Symbol(a), Symbol(b)]

//下面是另一个例子,Object.getOwnPropertySymbols方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。

const obj4_2 = {};

let foo4_2 = Symbol("foo4_2");

Object.defineProperty(obj4_2, foo4_2, {
    value: "foo4_2bar", //是这个属性的值,并不是Symbol里的描述
});

for (let i in obj4_2) {
    console.log(i); // 无输出
}

Object.getOwnPropertyNames(obj4_2); // []       返回的属性包括不可枚举的属性但不包括Symbol值作为名称的属性

Object.getOwnPropertySymbols(obj4_2); // [Symbol(foo4_2)]

//上面代码中,使用Object.getOwnPropertyNames方法得不到Symbol属性名,需要使用Object.getOwnPropertySymbols方法。

//另一个新的 API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。
let obj4_3 = {
    [Symbol('my_key')]: 1, //因为用了Symbol属性,所以必须用方括号,不然就成了字符串。在上面有讲过这个"必须放在方括号之中"。
    enum: 2,
    nonEnum: 3
};

Reflect.ownKeys(obj4_3); //  ["enum", "nonEnum", Symbol(my_key)]        Symbol的键名都是Symbol加上括号里的描述符

//由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

let size4_4 = Symbol('size4_4');

class Collection { //定义了一个类,附有3个方法
    constructor() {
        this[size4_4] = 0; //定义size4_4的值为0,size4_4是一个键。
    }

    add(item) {
        this[this[size4_4]] = item; //相当于键位下面是自增的,组成了一个类数组的对象,0位是item,下次再传进来的1是item
        this[size4_4]++; //传进来一个值,add的size4_4自增一个键位。在控制台查看x4_4,会看到里面的Symbol属性描述是size4_4的值为2。
    }

    static sizeOf4_4(instance) { //加上了static关键字之后表示静态方法,不会被继承,只能通过类来调用
        return instance[size4_4]; //传进来的参数是一个新的类,返回的是这个新的类里Symbol属性描述符为size4_4的值
    }
}

let x4_4 = new Collection(); //实例化类,实例化以后才能调用类里的非静态方法
Collection.sizeOf4_4(x4_4); // 0    没有调用add添加属性,所以是0

x4_4.add('foo');
x4_4.add('foo1');
Collection.sizeOf4_4(x4_4); // 2    调用了两次add添加属性,所以返回的Symbol值是2

Object.keys(x4_4); // ["0", "1"]    在add里第一行是为传进来的值增加键位和值,第二行是增加Symbol的值。调用了两次add,所以有两个键位
Object.getOwnPropertyNames(x4_4); // ["0", "1"]
Object.getOwnPropertySymbols(x4_4); // [Symbol(size4_4)]    查到键位之后,x4_4[size4_4]这样访问Symbol的值。
//上面代码中,对象x的size属性是一个 Symbol 值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。


//5、Symbol.for(),Symbol.keyFor()
//有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
let s5_1 = Symbol.for('foo');
let s5_2 = Symbol.for('foo');

s5_1 === s5_2; // true
//上面代码中,s1和s2都是 Symbol 值,但是它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值。

//Symbol.for()与Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。
Symbol.for("bar") === Symbol.for("bar"); // true

Symbol("bar") === Symbol("bar"); // false
//上面代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。
//********************************重要:Symbol.for和Symbol的区别***************************************
//因为Symbol.for是登记在全局环境里的,所以外部可以引用,Symbol却不是,Symbol每次调用都会返回一个不同的值,这也就看出了Symbol.for和Symbol的区别,for是在全局环境里登记了一个值,可以用多个变量来使用这个值,比如v1和v2都等于Symbol.for('foo'),这样他俩就是同一个值,而Symbol每次生成的都是一个唯一的值,不会重复,所以上面的Symbol("bar") === Symbol("bar")结果是false

//Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key。
let s5_3 = Symbol.for("foo"); //这个登记了,也就是在全局环境里能找到,是用Symbol.for声明的,返回这个Symbol的描述符。
Symbol.keyFor(s5_3) // "foo"

let s5_4 = Symbol("foo");
Symbol.keyFor(s5_4); // undefined
//上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined。

//需要注意的是,Symbol.for为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。
let iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo'); // true   console.log左上角选index.html,也就是当前页面,这样才不会报错,不要选top,因为选top之后如果url是file:///开头的话就会报错,放到本地服务器里就可以了。


//6、实例:模块的Singleton模式
//Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。
//对于 Node 来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?

//很容易想到, 可以把实例放到顶层对象global。
// mod.js
/*
function A() {
    this.foo = 'hello';
}

if (!global._foo) {     //global是node.js的方法
    global._foo = new A();
}

module.exports = global._foo;

//然后,加载上面的mod.js。

const a = require('./mod.js');
console.log(a.foo);
*/
//上面代码中,变量a任何时候加载的都是A的同一个实例。

//但是,这里有一个问题,全局变量global._foo是可写的,任何文件都可以修改。
/*
const a = require('./mod.js');
global._foo = 123;
*/
//上面的代码,会使得别的脚本加载mod.js都失真。

//为了防止这种情况出现,我们就可以使用 Symbol。
/*
// mod.js
const FOO_KEY = Symbol.for('foo');

function A() {
  this.foo = 'hello';
}

if (!global[FOO_KEY]) {
  global[FOO_KEY] = new A();
}

module.exports = global[FOO_KEY];
*/
//上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。
//const a = require('./mod.js');
//global[Symbol.for('foo')] = 123;

//如果键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写。

/*
// mod.js
//const FOO_KEY = Symbol('foo');        //因为Symbol.for是登记在全局环境里的,所以外部可以引用,Symbol却不是,Symbol每次调用都会返回一个不同的值,这也就看出了Symbol.for和Symbol的区别,for是在全局环境里登记了一个值,可以用多个变量来使用这个值,比如v1和v2都等于Symbol.for('foo'),这样他俩就是同一个值,而Symbol每次生成的都是一个唯一的值,不会重复,所以上面的Symbol("bar") === Symbol("bar")结果是false。

function A() {
    this.foo = 'hello';
  }
  
  if (!global[FOO_KEY]) {
    global[FOO_KEY] = new A();
  }
  
module.exports = global[FOO_KEY];
*/
//上面代码将导致其他脚本都无法引用FOO_KEY。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的FOO_KEY都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是完全可靠。


//7、内置的 Symbol 值
//除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
//7.1  Symbol.hasInstance       相当于修改了instanceof的判断方法,用来判断某个值是否符合要求。
//对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)。

class MyClass {
    [Symbol.hasInstance](foo) {
        return foo instanceof Array; //判断传进来的是不是array类型
    }
}

[1, 2, 3] instanceof new MyClass(); // true
//上面代码中,MyClass是一个类,new MyClass()会返回一个实例。该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。
//下面是另一个例子。

class Even {
    static[Symbol.hasInstance](obj) {
        return Number(obj) % 2 === 0; //求余数是否等于0,也就是是否能被2整除
    }
}

// 等同于
const Even1 = {
    [Symbol.hasInstance](obj) {
        return Number(obj) % 2 === 0;
    }
};

1 instanceof Even1; // false
2 instanceof Even1; // true
12345 instanceof Even1; // false

//7.2   Symbol.isConcatSpreadable       设置数组是否可展开,可以用来拼接成嵌套数组
//对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。
let arr72_1 = ['c', 'd'];
['a', 'b'].concat(arr72_1, 'e') // ['a', 'b', 'c', 'd', 'e']
arr72_1[Symbol.isConcatSpreadable]; // undefined

let arr72_2 = ['c', 'd'];
arr72_2[Symbol.isConcatSpreadable] = false;
['a', 'b'].concat(arr72_2, 'e'); // ['a', 'b', ['c','d'], 'e']      没有展开,把arr72_2放在了一个下标上,这样可以用来嵌套数组用,嵌套进去就可以拼接成二维数组了。
//上面代码说明,数组的默认行为是可以展开,Symbol.isConcatSpreadable默认等于undefined。该属性等于true时,也有展开的效果。
//******************************类数组和数组相反,类数组默认是不能展开的******************************
//类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpreadable属性设为true,才可以展开。(因为默认是不可展开的,false是不可展开,所以相当于默认是false,而数组是默认可以展开的,所以虽然是默认undefined,但跟设置为true是一样的)
let obj72_3 = { length: 2, 0: 'c', 1: 'd' };
['a', 'b'].concat(obj72_3, 'e'); // ['a', 'b', obj72_3, 'e']    obj72_3的位置的值是{0: "c", 1: "d", length: 2}

obj72_3[Symbol.isConcatSpreadable] = true;
['a', 'b'].concat(obj72_3, 'e'); // ['a', 'b', 'c', 'd', 'e']

//Symbol.isConcatSpreadable属性也可以定义在类里面。
class A72_4 extends Array { //extends是继承
    constructor(args) {
        super(args); //super是关键字,和typeof一样,super(parent),这样就已经在当前作用域里存在了super指向的parent的原型,前提是当前方法是继承了parent,因为super是指向父级对象。,在这里相当于直接调用了数组的方法,构造一个数组,所以下面的[0]=3才会变成数组。
        this[Symbol.isConcatSpreadable] = true;
    }
}
class A72_6 extends Array {
    constructor(args) {
        super(args);
    }
    get[Symbol.isConcatSpreadable]() { //get方法,数组不可展开,所以下面5和6在一个数组里
        return false;
    }
}
let a72_5 = new A72_4();
a72_5[0] = 3;
a72_5[1] = 4;
let a72_7 = new A72_6();
a72_7[0] = 5;
a72_7[1] = 6;
[1, 2].concat(a72_5).concat(a72_7); // [1, 2, 3, 4, [5, 6]]
//上面代码中,类A1是可展开的,类A2是不可展开的,所以使用concat时有不一样的结果。

//注意,Symbol.isConcatSpreadable的位置差异,A72_4是定义在实例上,A72_6是定义在类本身,效果相同。

//7.3   Symbol.species  用来调用一个类的继承对象,并让这个类实例化是继承了这个这个类继承的函数。
//对象的Symbol.species属性,指向一个构造函数。创建造衍生对象时,会使用该属性。
class MyArray73_1 extends Array {}

const a73_1 = new MyArray73_1();
a73_1.map(x => x) instanceof MyArray73_1; // true
//上面代码中,子类MyArray继承了父类Array。a.map(x => x)会创建一个MyArray的衍生对象,该衍生对象还是MyArray的实例。(map会返回一个新数组,也就是上面说的衍生对象,也是MyArray73_1的实例)

//现在,MyArray设置Symbol.species属性。
class MyArray73_2 extends Array {
    static get[Symbol.species]() { return Array; }
}
//上面代码中,由于定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的的函数,作为构造函数。这个例子也说明,定义Symbol.species属性要采用get读取器。默认的Symbol.species属性等同于下面的写法。
/*
static get [Symbol.species]() {
    return this;
  }
*/
//现在,再来看前面的例子。
class MyArray73_3 extends Array {
    static get[Symbol.species]() { return Array; } //也就是返回继承的函数,也就是Array。
}

const a73_3 = new MyArray73_3();
a73_3.map(x => x) instanceof MyArray73_3; // false
a73_3.map(x => x) instanceof Array; // true
//上面代码中,a.map(x => x)创建的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

//再看一个例子。
class T73_4 extends Promise {} //下面T73_4实例化以后是继承的T73_4,因为是T73_4的实例。

class T73_5 extends Promise {
    static get[Symbol.species]() {
        return Promise; //而定义了Symbol.species之后,继承的是Promise,所以instanceof检测T73_5是false,检测Promise就是true了。
    }
}

// new T73_4(r => r()).then(v => v) instanceof T73_4; // true      编译成es5会报错
// new T73_5(r => r()).then(v => v) instanceof T73_5; // false
//上面代码中,T2定义了Symbol.species属性,T1没有。结果就导致了创建衍生对象时(then方法),T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。

//总之,Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。


//7.4   Symbol.match    内置属性用来对传进来的参数进行操作或匹配
//对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。
let regexp74_2 = /\d+/g;
String.prototype.match(regexp74_2); // 等同于
regexp74_2[Symbol.match](this);

class MyMatcher74_1 {
    [Symbol.match](string) {
        return 'hello world'.indexOf(string); //如果有Symbol.match方法的话会优先调用,然后返回该方法的返回值。可以用来重复匹配接收的数据是否符合等。
    }
}

'e'.match(new MyMatcher74_1()); // 1

//7.5   Symbol.replace  定义一个replace方法并优先调用
//对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。
let searchValue = /\d+/g;
let replaceValue = "d";
String.prototype.replace(searchValue, replaceValue); // 等同于
searchValue[Symbol.replace](this, replaceValue); //跟上面的match一样,都是基于正则的,调用replace方法的时候会优先调用这个。
//下面是一个例子。
const x74_3 = {};
x74_3[Symbol.replace] = (...s) => console.log(s); //在这个属性上定义了一个方法,把传进来的参数全打印出来,是方括号属性表示法,相当于x72_6.Symbol.replace,但是由于是一个Symbol的replace属性,所以要放在方括号里。这样在这个属性上定义了一个方法,调用replace的时候会优先调用这个方法。二这个方法接收两个参数,第一个参数是正在作用的对象,第二个参数是要用来替换的值。

'Hello'.replace(x74_3, 'World'); // ["Hello", "World"]
//Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World。

//7.6   Symbol.search   定义search方法匹配字符串
//对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。
String.prototype.search(regexp74_2);
// 等同于
regexp74_2[Symbol.search](this);

class MySearch75_3 {
    constructor(value) {
        this.value = value;
    };
    [Symbol.search](string) {
        return string.indexOf(this.value); //返回首次出现的位置
    }
}
'foobar'.search(new MySearch75_3('foo')); // 0

//7.7   Symbol.split        配置一个对象是split方法,当调用的时候根据配置里的返回值来进行返回
//对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。
let separator = "";
let limit = 10;
//String.prototype.split(separator, limit); //用作分隔符的字符串和限制的长度
// 等同于
//separator[Symbol.split](this, limit); //只是语法,取消注释会报错,因为很多字符串和数组的方法在ES6以后都定义到Array和String的原型上了,所以在这的语法写了prototype。
//下面是一个例子。
class MySplitter75_4 {
    constructor(value) {
        this.value = value;
    };
    [Symbol.split](string) {
        let index = string.indexOf(this.value); //传进来的值在字符串里首次出现的位置
        if (index === -1) { //如果没匹配到字符串则返回原字符串
            return string;
        }
        return [ //返回一个数组,第一个位置是0开始到匹配完成的字符串索引位置的值,也就是与匹配的字符串相符的位置之前的值,第二个位置是是从匹配位置到结束的值,也就是从匹配完成的位置到结束的值,这个方法是返回了与字符串不匹配的所有字符串的值。
            string.substr(0, index),
            string.substr(index + this.value.length)
        ];
    }
}

'foobar'.split(new MySplitter75_4('foo')); // ['', 'bar']

'foobar'.split(new MySplitter75_4('bar')); // ['foo', '']

'foobar'.split(new MySplitter75_4('baz')); // 'foobar'
//上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为。

//7.8   Symbol.iterator 定义一个对象的遍历器方法
//对象的Symbol.iterator属性,指向该对象的默认遍历器方法。
let myIterable77_1 = { a: "val1", length: 1 };
myIterable77_1[Symbol.iterator] = function*() {
    yield 1;
    yield 2;
    yield 3;
};

[...myIterable77_1]; // [1, 2, 3]
console.log([...myIterable77_1]);
//https://github.com/babel/babel/blob/master/packages/babel-plugin-transform-runtime/src/definitions.js
//在这能看到babel转码和不转吗的方法,Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API ,比如 Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise 等全局对象,以及一些定义在全局对象上的方法(比如 Object.assign)都不会转码
//https://segmentfault.com/q/1010000005596587/

//最直接的方法就是npm安装的polyfill目录下有个polyfill.js,引用就可以了

//缺少babel编译插件,百度搜regeneratorRuntime is not defined。https://segmentfault.com/q/1010000006801859

//对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器,详细介绍参见《Iterator 和 for...of 循环》一章。
class Collection77_2 { * [Symbol.iterator]() { //定义一个Generator函数,定义一个索引i为0,从头开始判断当前索引不是未定义,则利用yield一步一步往下执行。
        let i = 0;
        while (this[i] !== undefined) {
            yield this[i];
            ++i;
        }
    }
}

let myCollection77_2 = new Collection77_2(); //实例化这个类,然后设置下标0和1位的值。在下面用for of遍历
myCollection77_2[0] = 1;
myCollection77_2[1] = 2;

for (let value of myCollection77_2) {
    console.log(value);
}
// 1
// 2

//7.9   Symbol.toPrimitive      当前对象参与运算时根据需要是字符串场景或数字场景或两者皆可的场景进行属性里要求的操作。
//对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

// Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

// Number:该场合需要转成数值
// String:该场合需要转成字符串
// Default:该场合可以转成数值,也可以转成字符串
let obj78_1 = {
    [Symbol.toPrimitive](hint) {
        switch (hint) {
            case 'number': //当前运算模式为数字运算
                return 123;
            case 'string': //当前运算模式为字符串运算
                return 'str';
            case 'default': //字符串和数字都可以,加法运算应该是只需要转换为字符串就可以,如果是两个数字相加,可以这样:3+Number(obj78_1)。
                return 'default';
            default:
                throw new Error();
        }
    }
};

2 * obj78_1; // 246
3 + obj78_1; // '3default'
obj78_1 == 'default'; // true
String(obj78_1); // 'str'       //字符串运算


//7.10   Symbol.toStringTag  定义调用Object.prototype.toString返回的[object Object]中第二个值的类型,比如下面的返回[object xxx]

//对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]中object后面的那个字符串。
// 例一
({
    [Symbol.toStringTag]: 'Foo'
}.toString()); // "[object Foo]"

// 例二
class Collection79_2 {
    get[Symbol.toStringTag]() {
        return 'xxx';
    }
};
let x79_2 = new Collection79_2();
Object.prototype.toString.call(x79_2); // "[object xxx]"
/*
ES6 新增内置对象的Symbol.toStringTag属性值如下。

JSON[Symbol.toStringTag]:'JSON'
Math[Symbol.toStringTag]:'Math'
Module 对象M[Symbol.toStringTag]:'Module'
ArrayBuffer.prototype[Symbol.toStringTag]:'ArrayBuffer'
DataView.prototype[Symbol.toStringTag]:'DataView'
Map.prototype[Symbol.toStringTag]:'Map'
Promise.prototype[Symbol.toStringTag]:'Promise'
Set.prototype[Symbol.toStringTag]:'Set'
%TypedArray%.prototype[Symbol.toStringTag]:'Uint8Array'等
WeakMap.prototype[Symbol.toStringTag]:'WeakMap'
WeakSet.prototype[Symbol.toStringTag]:'WeakSet'
%MapIteratorPrototype%[Symbol.toStringTag]:'Map Iterator'
%SetIteratorPrototype%[Symbol.toStringTag]:'Set Iterator'
%StringIteratorPrototype%[Symbol.toStringTag]:'String Iterator'
Symbol.prototype[Symbol.toStringTag]:'Symbol'
Generator.prototype[Symbol.toStringTag]:'Generator'
GeneratorFunction.prototype[Symbol.toStringTag]:'GeneratorFunction'
*/

//7.11  Symbol.unscopables  用来使用with语句的时候排除传进来参数的某个属性

//对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。
//with的作用
/*
var a = 123;
var b = {a : 321};
with(b){
    console.log(a); // 321      //b是with的语境,如果b没有a属性,那就往上一级查找
}

var a = 123;
var b = {}; 这里去掉b中的a属性
with(b){
    console.log(a); // 123
}
Array.prototype[Symbol.unscopables];
*/
// {
//   copyWithin: true,
//   entries: true,
//   fill: true,
//   find: true,
//   findIndex: true,
//   includes: true,
//   keys: true
// }

Object.keys(Array.prototype[Symbol.unscopables]);
// ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
//上面代码说明,数组有 7 个属性,会被with命令排除。

/*
//不建议使用with语句,因为with语句无法在编译时就确定,属性到底归属哪个对象。而且ES5的严格模式直接禁用了with语句。所以下面的代码取消注释以后如果编译为ES5会报错。
// 没有 unscopables 时
class MyClass711_1 {
    foo711_1() { return 1; }
}

var foo711_2 = function() { return 2; };

with(MyClass711_1.prototype) {
    foo711_2(); // 1
}

// 有 unscopables 时
class MyClass711_2 {
    foo711_2() { return 1; }
    get[Symbol.unscopables]() {
        return { foo711_2: true }; //设置为true则用with的时候排除这条属性,所以查找的全局作用域。
    }
}

var foo711_2 = function() { return 2; };

with(MyClass711_2.prototype) {
    foo711_2(); // 2
}
*/

猜你喜欢

转载自my.oschina.net/lmqswp/blog/1820486
今日推荐