JavaScript高级程序设计(第4版)读书笔记:四

JavaScript程序设计

温馨提示,Symbol类型这段较长,不必追求一次性看懂

2.3.7 Symbol 类型

Symbol(符号)是 ECMAScript 6 新增的数据类型。符号是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险。

尽管听起来跟私有属性有点类似,但符号并不是为了提供私有属性的行为才增加的(尤其是因为Object API 提供了方法,可以更方便地发现符号属性)。相反,符号就是用来创建唯一记号,进而用作非字符串形式的对象属性。

  1. 符号的基本用法

    符号需要使用 Symbol()函数初始化。因为符号本身是原始类型,所以 typeof 操作符对符号返回symbol

    let sym = Symbol(); 
    console.log(typeof sym); // symbol
    
    // 调用 Symbol()函数时,也可以传入一个字符串参数作为对符号的描述(description)
    // 将来可以通过这个字符串来调试代码。但是,这个字符串参数与符号定义或标识完全无关
    
    let genericSymbol = Symbol(); 
    let otherGenericSymbol = Symbol(); 
    let fooSymbol = Symbol('foo'); 
    let otherFooSymbol = Symbol('foo'); 
    console.log(genericSymbol == otherGenericSymbol); // false
    console.log(fooSymbol == otherFooSymbol); // false
    

    符号没有字面量语法,这也是它们发挥作用的关键。按照规范,你只要创建 Symbol()实例并将其用作对象的新属性,就可以保证它不会覆盖已有的对象属性,无论是符号属性还是字符串属性。

    let genericSymbol = Symbol(); 
    console.log(genericSymbol); // Symbol() 
    let fooSymbol = Symbol('foo'); 
    console.log(fooSymbol); // Symbol(foo);
    

    最重要的是,Symbol()函数不能与 new 关键字一起作为构造函数使用。这样做是为了避免创建符号包装对象,像使用 Boolean、String 或 Number 那样,它们都支持构造函数且可用于初始化包含原始值的包装对象:

    let myBoolean = new Boolean(); 
    console.log(typeof myBoolean); // "object" 
    let myString = new String(); 
    console.log(typeof myString); // "object" 
    let myNumber = new Number(); 
    console.log(typeof myNumber); // "object"
    let mySymbol = new Symbol(); // TypeError: Symbol is not a constructor
    

    如果你确实想使用符号包装对象,可以借用 Object()函数:

    let mySymbol = Symbol(); 
    let myWrappedSymbol = Object(mySymbol); 
    console.log(typeof myWrappedSymbol); // "object"
    
  2. 使用全局符号注册表

    如果运行时的不同部分需要共享和重用符号实例,那么可以用一个字符串作为键,在全局符号注册表中创建并重用符号。为此,需要使用Symbol.for()方法。

    let fooGlobalSymbol = Symbol.for('foo'); 
    console.log(typeof fooGlobalSymbol); // symbol
    

    Symbol.for()对每个字符串键都执行幂等操作。第一次使用某个字符串调用时,它会检查全局运行时注册表,发现不存在对应的符号,于是就会生成一个新符号实例并添加到注册表中。后续使用相同字符串的调用同样会检查注册表,发现存在与该字符串对应的符号,然后就会返回该符号实例。

    let fooGlobalSymbol = Symbol.for('foo'); // 创建新符号
    let otherFooGlobalSymbol = Symbol.for('foo'); // 重用已有符号
    console.log(fooGlobalSymbol === otherFooGlobalSymbol); // true
    

    即使采用相同的符号描述,在全局注册表中定义的符号跟使用 Symbol()定义的符号也并不等同:

    let localSymbol = Symbol('foo'); 
    let globalSymbol = Symbol.for('foo'); 
    console.log(localSymbol === globalSymbol); // false
    

    全局注册表中的符号必须使用字符串键来创建,因此作为参数传给 Symbol.for()的任何值都会被转换为字符串。此外,注册表中使用的键同时也会被用作符号描述。

    let emptyGlobalSymbol = Symbol.for(); 
    console.log(emptyGlobalSymbol); // Symbol(undefined)
    

    还可以使用 Symbol.keyFor()来查询全局注册表,这个方法接收符号,返回该全局符号对应的字符串键。如果查询的不是全局符号,则返回 undefined。

    // 创建全局符号
    let s = Symbol.for('foo'); 
    console.log(Symbol.keyFor(s)); // foo 
    // 创建普通符号
    let s2 = Symbol('bar'); 
    console.log(Symbol.keyFor(s2)); // undefined
    

    如果传给 Symbol.keyFor()的不是符号,则该方法抛出 TypeError:

    Symbol.keyFor(123); // TypeError: 123 is not a symbol
    
  3. 使用符号作为属性

    凡是可以使用字符串或数值作为属性的地方,都可以使用符号。这就包括了对象字面量属性Object.defineProperty()Object.defineProperties()定义的属性。对象字面量只能在计算属

    性语法中使用符号作为属性。

    let s1 = Symbol('foo'), 
     s2 = Symbol('bar'), 
     s3 = Symbol('baz'), 
     s4 = Symbol('qux'); 
    let o = { 
    // 此处使用的语法为计算属性名(Computed Property Name)
    // 在ES6中,对象字面量允许使用方括号来包裹表达式,计算结果作为属性名。
    // 这里[s1]表示使用变量s1的值(一个Symbol)作为属性名,对应的值为'foo val'。
     [s1]: 'foo val' 
    }; 
    // 这样也可以:o[s1] = 'foo val'; 
    console.log(o); 
    // {Symbol(foo): foo val} 
    Object.defineProperty(o, s2, {value: 'bar val'}); 
    console.log(o); 
    // {Symbol(foo): foo val, Symbol(bar): bar val} 
    Object.defineProperties(o, { 
     [s3]: {value: 'baz val'}, 
     [s4]: {value: 'qux val'} 
    }); 
    console.log(o); 
    // {Symbol(foo): foo val, Symbol(bar): bar val, 
    // Symbol(baz): baz val, Symbol(qux): qux val}
    

    这里来讲讲Object.definePropertyObject.defineProperties两个方法:

    1. Object.defineProperty

      // 基本语法:
      Object.defineProperty(obj, property, descriptor)
      // 对于上例中的代码
      Object.defineProperty(o, s2, { value: 'bar val' });
      // 这行代码的意思是:
      // 给对象 o
      // 添加一个属性,属性名为 Symbol s2
      // 属性描述符为 { value: 'bar val' }
      

      属性描述符:

      属性描述符有两种类型:数据描述符存取描述符

      • 数据描述符

        // 数据描述符(常用)
        Object.defineProperty(o, s2, {
          value: 'bar val',      // 属性值
          writable: true,        // 是否可修改(默认false)
          enumerable: true,      // 是否可枚举(默认false)
          configurable: true     // 是否可删除、可修改描述符(默认false)
        });
        
      • 存取描述符(getter/setter)

        // 这里的get(), set()
        Object.defineProperty(o, s2, {
          get() { return this._internalValue; },
          set(newValue) { this._internalValue = newValue; },
          enumerable: true,
          configurable: true
        });
        
    2. Object.defineProperties

      // 基本语法
      Object.defineProperties(obj, propertiesObject)
      // 对于上例中的代码
      Object.defineProperties(o, {
          [s3]: { value: 'baz val' },
          [s4]: { value: 'qux val' }
      });
      // 实际上就是一次性添加多个属性
      
    3. 与普通属性赋值的区别

      普通赋值(宽松)

      // 属性默认:可写、可枚举、可配置
      o.regularProp = 'value';
      o[s1] = 'value';
      

      defineProperty(精确控制)

      // 属性默认:不可写、不可枚举、不可配置(除非显式指定)
      Object.defineProperty(o, s1, { value: 'value' });
      

    类似于 Object.getOwnPropertyNames()返回对象实例的常规属性数组,Object.getOwnPropertySymbols()返回对象实例的符号属性数组。这两个方法的返回值彼此互斥。Object.getOwnPropertyDescriptors()会返回同时包含常规和符号属性描述符的对象。Reflect.ownKeys()会返回两种类型的键:

    let s1 = Symbol('foo'), 
     s2 = Symbol('bar'); 
    let o = { 
     [s1]: 'foo val', 
     [s2]: 'bar val', 
     baz: 'baz val', 
     qux: 'qux val' 
    }; 
    console.log(Object.getOwnPropertySymbols(o)); 
    // [Symbol(foo), Symbol(bar)] 
    console.log(Object.getOwnPropertyNames(o)); 
    // ["baz", "qux"] 
    console.log(Object.getOwnPropertyDescriptors(o)); 
    // {baz: {...}, qux: {...}, Symbol(foo): {...}, Symbol(bar): {...}} 
    console.log(Reflect.ownKeys(o)); 
    // ["baz", "qux", Symbol(foo), Symbol(bar)]
    

    因为符号属性是对内存中符号的一个引用,所以直接创建并用作属性的符号不会丢失。但是,如果没有显式地保存对这些属性的引用,那么必须遍历对象的所有符号属性才能找到相应的属性键:

    let o = { 
     [Symbol('foo')]: 'foo val', 
     [Symbol('bar')]: 'bar val' 
    }; 
    console.log(o); 
    // {Symbol(foo): "foo val", Symbol(bar): "bar val"} 
    let barSymbol = Object.getOwnPropertySymbols(o) 
     .find((symbol) => symbol.toString().match(/bar/)); 
    console.log(barSymbol); 
    // Symbol(bar)
    
  4. 常用内置符号

    ECMAScript 6 也引入了一批常用内置符号(well-known symbol),用于暴露语言内部行为,开发者可以直接访问、重写或模拟这些行为。这些内置符号都以 Symbol 工厂函数字符串属性的形式存在。

    这些内置符号最重要的用途之一是重新定义它们,从而改变原生结构的行为。比如,我们知道for-of 循环会在相关对象上使用Symbol.iterator属性,那么就可以通过在自定义对象上重新定义Symbol.iterator 的值,来改变 for-of 在迭代该对象时的行为。

    // 假设我们有一个对象,我们希望它能够被 for...of 循环遍历,我们可以这样做:
    // 定义一个对象,并实现 Symbol.iterator 方法。
    // 该方法必须返回一个迭代器对象,这个迭代器对象有一个 next 方法。
    // next 方法返回一个包含 value 和 done 属性的对象。
    let myIterableObject = {
        data: [1, 2, 3, 4, 5],
        // 实现 Symbol.iterator 方法
        [Symbol.iterator]: function() {
            let index = 0;
            let data = this.data;
            // 返回一个迭代器对象
            return {
                next: function() {
                    if (index < data.length) {
                        return { value: data[index++], done: false };
                    } else {
                        return { value: undefined, done: true };
                    }
                }
            };
        }
    };
    
    // 现在可以用 for...of 循环遍历这个对象了
    for (let item of myIterableObject) {
        console.log(item);
    }
    // 输出:1, 2, 3, 4, 5
    

    这些内置符号也没有什么特别之处,它们就是全局函数 Symbol 的普通字符串属性,指向一个符号的实例。所有内置符号属性都是不可写、不可枚举、不可配置的。

    Note: 在提到 ECMAScript 规范时,经常会引用符号在规范中的名称,前缀为@@。比如,@@iterator 指的就是 Symbol.iterator。

  5. Symbol.asyncIterator

    根据 ECMAScript 规范,这个符号作为一个属性表示“**一个方法,该方法返回对象默认的 AsyncIterator。**由for-await-of语句使用”。换句话说,这个符号表示实现异步迭代器 API 的函数。for-await-of 循环会利用这个函数执行异步迭代操作。循环时,它们会调用以 Symbol.asyncIterator为键的函数,并期望这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API的 AsyncGenerator

    class Foo { 
    // async - 异步函数标识,表示这个函数是异步的,可以在函数内部使用 await,函数总是返回一个 Promise
    // * - 生成器函数标识,表示这是一个生成器函数,可以在函数内部使用 yield,函数返回一个 Generator 对象
    // [Symbol.asyncIterator] - 计算属性名,使用 Symbol.asyncIterator 作为方法名
    // () {} - 方法定义,普通的函数参数列表和方法体
     async *[Symbol.asyncIterator]() {} 
    } 
    let f = new Foo(); 
    console.log(f[Symbol.asyncIterator]()); 
    // AsyncGenerator {<suspended>}
    
    // 关于生成器函数:生成器函数是一种特殊的函数,它可以暂停执行并在之后恢复执行。与普通函数一旦调用就必须执行到底不同,生成器函数可以在执行过程中多次暂停和继续。
    function* demoGenerator() {
        console.log("第1步");
        const result1 = yield "暂停点1";
        console.log("第2步,收到:", result1);
        
        const result2 = yield "暂停点2";
        console.log("第3步,收到:", result2);
        
        return "完成";
    }
    
    const generator = demoGenerator();
    
    // 第一次调用 next()
    console.log("调用第1次 next():");
    let step1 = generator.next();
    console.log("返回:", step1);
    // 输出:
    // 第1步
    // 返回值: { value: "暂停点1", done: false }
    
    // 第二次调用 next(),并传入值
    console.log("\n调用第2次 next('值1'):");
    let step2 = generator.next("值1");
    console.log("返回:", step2);
    // 输出:
    // 第2步,收到: 值1
    // 返回值: { value: "暂停点2", done: false }
    
    // 第三次调用 next(),并传入值
    console.log("\n调用第3次 next('值2'):");
    let step3 = generator.next("值2");
    console.log("返回:", step3);
    // 输出:
    // 第3步,收到: 值2
    // 返回值: { value: "完成", done: true }
    
    /*
    	生成器函数的特点:使用 function* 声明,可以多次暂停和恢复执行,返回一个生成器对象(也是迭代器)
    	yield 关键字的作用:
    	1.暂停执行:函数在执行到 yield 时暂停
    	2.返回值:向调用者返回 yield 后面的值
    	3.接收值:当恢复执行时,接收通过 next() 传入的值
    	4.双向通信:既是数据的生产者,也是数据的消费者
    */
    

    技术上,这个由 Symbol.asyncIterator 函数生成的对象应该通过其 next()方法陆续返回Promise 实例。可以通过显式地调用 next()方法返回,也可以隐式地通过异步生成器函数返回:

    class Emitter { 
       constructor(max) { 
         this.max = max; 
         this.asyncIdx = 0; 
       } 
       async *[Symbol.asyncIterator]() { 
         while(this.asyncIdx < this.max) { 
           yield new Promise((resolve) => resolve(this.asyncIdx++)); 
         } 
       } 
    } 
    async function asyncCount() { 
       let emitter = new Emitter(5); 
       for await(const x of emitter) { 
         console.log(x); 
       } 
    } 
    asyncCount(); 
    // 0 
    // 1 
    // 2 
    // 3 
    // 4
    

    Note: Symbol.asyncIterator 是 ES2018 规范定义的,因此只有版本非常新的浏览器支持它。

  6. Symbol.hasInstance

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法决定一个构造器对象是否认可一个对象是它的实例。由 instanceof 操作符使用”。instanceof 操作符可以用来确定一个对象实例的原型链上是否有原型。instanceof 的典型使用场景如下:

    function Foo() {} 
    let f = new Foo(); 
    console.log(f instanceof Foo); // true 
    class Bar {} 
    let b = new Bar(); 
    console.log(b instanceof Bar); // true
    

    在 ES6 中,instanceof 操作符会使用 Symbol.hasInstance 函数来确定关系。以 Symbol.hasInstance 为键的函数会执行同样的操作,只是操作数对调了一下:

    function Foo() {} 
    let f = new Foo(); 
    console.log(Foo[Symbol.hasInstance](f)); // true 
    class Bar {} 
    let b = new Bar(); 
    console.log(Bar[Symbol.hasInstance](b)); // true
    

    这个属性定义在Function的原型上,因此默认在所有函数和类上都可以调用。由于 instanceof操作符会在**原型链(此处将会在后面详细解释,对这里有个印象即可)**上寻找这个属性定义,就跟在原型链上寻找其他属性一样,因此可以在继承的类上通过静态方法重新定义这个函数:

    class Bar {} 
    class Baz extends Bar { 
     static [Symbol.hasInstance]() { 
     return false; 
     } 
    } 
    let b = new Baz(); 
    console.log(Bar[Symbol.hasInstance](b)); // true 
    console.log(b instanceof Bar); // true 
    console.log(Baz[Symbol.hasInstance](b)); // false 
    console.log(b instanceof Baz); // false
    
  7. Symbol.isConcatSpreadable

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个布尔值,如果是 true,则意味着对象应该用 Array.prototype.concat()打平其数组元素("打平"指的是将嵌套的数组结构展开成一维数组的过程。)”。ES6 中的 Array.prototype.concat()方法会根据接收到的对象类型选择如何将一个类数组对象拼接成数组实例。覆盖 Symbol.isConcatSpreadable 的值可以修改这个行为。

    数组对象默认情况下会被打平到已有的数组,false 或假值会导致整个对象被追加到数组末尾。类数组对象默认情况下会被追加到数组末尾,true 或真值会导致这个类数组对象被打平到数组实例。其他不是类数组对象的对象在 Symbol.isConcatSpreadable 被设置为 true 的情况下将被忽略!!!

    let arr1 = [1, 2];
    let arr2 = [3, 4];
    
    // 1.数组默认会被"打平"
    let result = arr1.concat(arr2);
    console.log(result); // [1, 2, 3, 4] - arr2 被展开
    // 查看数组的 Symbol.isConcatSpreadable
    console.log(arr2[Symbol.isConcatSpreadable]); // undefined,但数组默认行为相当于 true
    
    // 2.阻止数组被"打平"
    arr2[Symbol.isConcatSpreadable] = false;
    let result = arr1.concat(arr2);
    console.log(result); // [1, 2, [3, 4]] - arr2 作为整体添加
    
    // 3.类数组对象的处理
    // 创建一个类数组对象
    let arrayLike = {
      0: 'a',
      1: 'b', 
      2: 'c',
      length: 3
    };
    let arr = [1, 2];
    // 类数组对象默认不会被展开
    let result1 = arr.concat(arrayLike);
    console.log(result1); // [1, 2, {0: 'a', 1: 'b', 2: 'c', length: 3}]
    
    // 设置 Symbol.isConcatSpreadable 为 true
    arrayLike[Symbol.isConcatSpreadable] = true;
    
    let result2 = arr.concat(arrayLike);
    console.log(result2); // [1, 2, 'a', 'b', 'c'] - 类数组被展开了!
    
    // 4.普通对象
    let initial = ["foo"];
    let obj = {age: 24}; // 普通对象
    let set = new Set(["x", "y", "z"]); // 普通对象
    console.log(initial.concat(obj)); // ['foo', {...}]
    console.log(initial.concat(set)); // ['foo', Set(3)]
    // 对普通对象设置Symbol.isConcatSpreadable为true
    obj[Symbol.isConcatSpreadable] = true; 
    set[Symbol.isConcatSpreadable] = true;
    console.log(initial.concat(obj)); // ['foo'] - obj被忽略!
    console.log(initial.concat(set)); // ['foo'] - set被忽略!
    // 再比如下面这个例子:
    let config = { debug: true, version: '1.0' };
    let user = { name: 'John', age: 30 };
    let uniqueItems = new Set([1, 2, 3]);
    config[Symbol.isConcatSpreadable] = true;
    user[Symbol.isConcatSpreadable] = true;
    uniqueItems[Symbol.isConcatSpreadable] = true;
    let result = [].concat(config, user, uniqueItems);
    console.log(result); // 输出:[] - config,user,uniqueItems都被忽略
    
  8. Symbol.iterator

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法返回对象默认的迭代器。由 for-of 语句使用”。换句话说,这个符号表示实现迭代器 API 的函数。for-of 循环这样的语言结构会利用这个函数执行迭代操作。循环时,它们会调用以 Symbol.iterator为键的函数,并默认这个函数会返回一个实现迭代器 API 的对象。很多时候,返回的对象是实现该 API的 Generator:

    // 类似于Symbol.asyncIterator,只不过Symbol.iterator是同步的
    
    class Foo { 
     *[Symbol.iterator]() {} 
    } 
    let f = new Foo(); 
    console.log(f[Symbol.iterator]()); 
    // Generator {<suspended>}
    

    技术上,这个由 Symbol.iterator 函数生成的对象应该通过其 next()方法陆续返回值。可以通过显式地调用 next()方法返回,也可以隐式地通过生成器函数返回:

    class Emitter { 
     constructor(max) { 
     this.max = max; 
     this.idx = 0; 
     } 
     *[Symbol.iterator]() { 
     while(this.idx < this.max) { 
     yield this.idx++; 
     } 
     } 
    } 
    function count() { 
     let emitter = new Emitter(5); 
     for (const x of emitter) { 
     console.log(x); 
     } 
    } 
    count(); 
    // 0
    // 1 
    // 2 
    // 3 
    // 4
    
  9. Symbol.match

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法用正则表达式去匹配字符串。由 String.prototype.match()方法使用”。String.prototype.match()方法会使用以 Symbol.match 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数

    console.log(RegExp.prototype[Symbol.match]); 
    // ƒ [Symbol.match]() { [native code] },即正则表达式的原型上默认有这个函数的定义
    
    // 正常用法:用正则表达式匹配字符串
    let result = 'hello world'.match(/world/);
    console.log(result); 
    // ["world", index: 6, input: "hello world", groups: undefined]
    

    给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象如果想改变这种行为,让方法直接使用参数,则可以重新定义 Symbol.match 函数以取代默认对正则表达式求值的行为,从而让match()方法使用非正则表达式实例。Symbol.match 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:

    class StringMatcher { 
       constructor(str) { 
           this.str = str; 
       } 
       [Symbol.match](target) { 
           return target.includes(this.str); 
       } 
    } 
    console.log('foobar'.match(new StringMatcher('foo'))); // true 
    console.log('barbaz'.match(new StringMatcher('qux'))); // false
    
    // 简单的邮箱验证
    class EmailValidator {
        [Symbol.match](target) {
            // 简单的邮箱验证
            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
            return emailRegex.test(target);
        }
    }
    
    const validator = new EmailValidator();
    console.log("test@example.com".match(validator)); // true
    console.log("invalid-email".match(validator));    // false
    
  10. Symbol.replace

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法替换一个字符串中匹配的子串。由 String.prototype.replace()方法使用”。String.prototype.replace()方法会使用以 Symbol.replace 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数

    console.log(RegExp.prototype[Symbol.replace]); 
    // ƒ [Symbol.replace]() { [native code] } ,即正则表达式的原型上默认有这个函数的定义
    console.log('foobarbaz'.replace(/bar/, 'qux')); 
    // 'fooquxbaz'
    

    给这个方法传入非正则表达式值会导致该值被转换为RegExp对象如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.replace 函数以取代默认对正则表达式求值的行为,从而让replace()方法使用非正则表达式实例。Symbol.replace 函数接收两个参数,即调用 replace()方法的字符串实例和替换字符串。返回的值没有限制:

    class StringReplacer { 
       constructor(str) { 
           this.str = str; 
       } 
       [Symbol.replace](target, replacement) { 
           return target.split(this.str).join(replacement); 
       } 
    } 
    console.log('barfoobaz'.replace(new StringReplacer('foo'), 'qux')); 
    // "barquxbaz"
    
  11. Symbol.search

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法返回字符串中匹配正则表达式的索引。由 String.prototype.search()方法使用”。String.prototype.search()方法会使用以 Symbol.search 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数

    console.log(RegExp.prototype[Symbol.search]); 
    // ƒ [Symbol.search]() { [native code] } ,即正则表达式的原型上默认有这个函数的定义
    console.log('foobar'.search(/bar/)); 
    // 3
    

    给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.search 函数以取代默认对正则表达式求值的行为,从而让search()方法使用非正则表达式实例。Symbol.search 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:

    class StringSearcher { 
       constructor(str) { 
           this.str = str; 
       } 
       [Symbol.search](target) { 
           return target.indexOf(this.str); 
       } 
    } 
    console.log('foobar'.search(new StringSearcher('foo'))); // 0 
    console.log('barfoo'.search(new StringSearcher('foo'))); // 3 
    console.log('barbaz'.search(new StringSearcher('qux'))); // -1
    
  12. Symbol.species

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个函数值,该函数作为创建派生对象(派生对象指的是通过某个方法从现有对象创建的新对象)的构造函数”。这个属性在内置类型中最常用,用于对内置类型实例方法的返回值暴露实例化派生对象的方法。用 Symbol.species 定义静态的获取器(getter)方法,可以覆盖新创建实例的原型定义:

    // 派生对象:通过某个方法从现有对象创建的新对象
    let arr1 = [1, 2, 3];
    let arr2 = arr1.concat(4, 5); // arr2 是 arr1 的派生对象
    let arr3 = arr1.map(x => x * 2); // arr3 也是派生对象
    
    // 当你继承内置类(如 Array)时,派生对象的类型可能会出问题:
    class MyArray extends Array {}
    
    let myArr = new MyArray(1, 2, 3);
    let result = myArr.concat(4, 5); // 应该返回 MyArray 还是普通 Array?
    
    // Symbol.species 就是用来指定创建派生对象时应该使用哪个构造函数。
    // 如果没有定义 Symbol.species,默认返回 this(当前构造函数)
    class Bar extends Array {} // 不使用Symbol.species
    class Baz extends Array { 
       static get [Symbol.species]() { 
           return Array; // 使用Symbol.species,明确指定创建派生对象时使用 Array,而不是 Baz
       } 
    } 
    let bar = new Bar(); 
    console.log(bar instanceof Array); // true - Bar 继承自 Array
    console.log(bar instanceof Bar); // true - bar 是 Bar 的实例
    bar = bar.concat('bar'); // 调用 concat 创建新数组
    console.log(bar instanceof Array); // true - 新数组是 Array
    console.log(bar instanceof Bar); // true - 同时新数组也是 Bar 的实例
    let baz = new Baz(); 
    console.log(baz instanceof Array); // true - Baz 继承自 Array
    console.log(baz instanceof Baz); // true - baz 是 Baz 的实例
    baz = baz.concat('baz'); 
    console.log(baz instanceof Array); // true - 新数组是 Array
    console.log(baz instanceof Baz); // false - 新数组不是 Baz 的实例!
    
  13. Symbol.split

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个正则表达式方法,该方法在匹配正则表达式的索引位置拆分字符串。由 String.prototype.split()方法使用”。String.prototype. split()方法会使用以 Symbol.split 为键的函数来对正则表达式求值。正则表达式的原型上默认有这个函数的定义,因此所有正则表达式实例默认是这个 String 方法的有效参数

    console.log(RegExp.prototype[Symbol.split]); 
    // ƒ [Symbol.split]() { [native code] } ,即正则表达式的原型上默认有这个函数的定义
    console.log('foobarbaz'.split(/bar/)); 
    // ['foo', 'baz']
    

    给这个方法传入非正则表达式值会导致该值被转换为 RegExp 对象如果想改变这种行为,让方法直接使用参数,可以重新定义 Symbol.split 函数以取代默认对正则表达式求值的行为,从而让 split()方法使用非正则表达式实例Symbol.split 函数接收一个参数,就是调用 match()方法的字符串实例。返回的值没有限制:

    class StringSplitter { 
       constructor(str) { 
           this.str = str; 
       } 
       [Symbol.split](target) { 
           return target.split(this.str); 
       } 
    } 
    console.log('barfoobaz'.split(new StringSplitter('foo'))); 
    // ["bar", "baz"]
    
  14. Symbol.toPrimitive

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个方法,该方法将对象转换为相应的原始值。由 ToPrimitive 抽象操作使用”。很多内置操作都会尝试强制将对象转换为原始值,包括字符串、数值和未指定的原始类型。对于一个自定义对象实例,通过在这个实例的 Symbol.toPrimitive 属性上定义一个函数可以改变默认行为。

    根据提供给这个函数的参数(string、number 或 default),可以控制返回的原始值:

    class Foo {} 
    let foo = new Foo(); 
    console.log(3 + foo); // "3[object Object]" 
    console.log(3 - foo); // NaN 
    console.log(String(foo)); // "[object Object]" 
    class Bar { 
       constructor() { 
           this[Symbol.toPrimitive] = function(hint) { 
               switch (hint) { 
                   case 'number': 
                   return 3; 
                   case 'string': 
                   return 'string bar'; 
                   case 'default': 
                   default: 
                   return 'default bar'; 
               } 
           } 
       } 
    }
    let bar = new Bar(); 
    console.log(3 + bar); // "3default bar" 
    console.log(3 - bar); // 0 
    console.log(String(bar)); // "string bar"
    
  15. Symbol.toStringTag

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个字符串,该字符串用于创建对象的默认字符串描述。由内置方法 Object.prototype.toString()使用”。通过 toString()方法获取对象标识时,会检索由 Symbol.toStringTag 指定的实例标识符,默认为"Object"。内置类型已经指定了这个值,但自定义类实例还需要明确定义:

    // 内置类型
    let s = new Set(); 
    console.log(s); // Set(0) {} 
    console.log(s.toString()); // [object Set] 
    console.log(s[Symbol.toStringTag]); // Set 
    // 未定义Symbol.toStringTag
    class Foo {} 
    let foo = new Foo(); 
    console.log(foo); // Foo {} 
    console.log(foo.toString()); // [object Object] 
    console.log(foo[Symbol.toStringTag]); // undefined 
    // 明确定义Symbol.toStringTag
    class Bar { 
       constructor() { 
           this[Symbol.toStringTag] = 'Bar'; 
       } 
    } 
    let bar = new Bar(); 
    console.log(bar); // Bar {} 
    console.log(bar.toString()); // [object Bar] 
    console.log(bar[Symbol.toStringTag]); // Bar
    
  16. Symbol.unscopables

    根据 ECMAScript 规范,这个符号作为一个属性表示“一个对象,该对象所有的以及继承的属性,都会从关联对象的 with 环境绑定中排除”。设置这个符号并让其映射对应属性的键值为 true,就可以阻止该属性出现在 with 环境绑定中,如下例所示:

    let o = { foo: 'bar' }; 
    with (o) { 
       console.log(foo); // bar 
    } 
    o[Symbol.unscopables] = { 
       foo: true 
    }; 
    with (o) { 
       console.log(foo); // ReferenceError 
    }
    

    Note: 不推荐使用 with,因此也不推荐使用 Symbol.unscopables。

### 回答1: 《JavaScript高级程序设计》第4是一本深入讲解JavaScript编程语言的书籍。该书详细介绍了JavaScript的基础知识、语法、面向对象编程、DOM操作、事件处理、Ajax、JSON等方面的内容。此外,该书还介绍了一些高级技术,如模块化编程、正则表达式、Web Workers、Web Storage等。该书适合有一定JavaScript基础的读者阅读,可以帮助读者深入了解JavaScript编程语言,提高编程技能。 ### 回答2: 《JavaScript高级程序设计》第4(以下简称《JS高级程序设计》)是由李炎恢编写的一部JavaScript语言的经典教材,它被誉为“JavaScript圣经”。本书全面深入地讲解了JavaScript语言的核心概念、高级特性和最佳实践,对于想要深入学习JavaScript的开发者来说是一本必读之作。 首先,本书从JavaScript的基础知识开始,包括JavaScript的数据类型、变量、运算符、函数等。随后,本书详细介绍了JavaScript的面向对象编程,包括对象、原型、继承等概念,以及使用构造函数和类来创建对象的方法。 其次,本书不仅讲述了JavaScript的基本语法,更详细深入地介绍了诸如函数表达式、闭包、高阶函数、递归等高级特性,对于想要提高自己的JavaScript编程能力的开发者很有帮助。 最后,本书也介绍了一些实际的开发技巧和最佳实践,例如DOM操作、事件处理、Ajax、JSON、模块化开发等,让开发者在实际的开发中更加得心应手。 总之,《JavaScript高级程序设计》第4是一本权威性的JavaScript经典教材,它涵盖了JavaScript的核心概念、高级特性和最佳实践,对于想要深入了解JavaScript的开发者来说是一本必读之作。无论是初学者还是有经验的开发者,都可以从中找到大量有用的知识和实践经验。 ### 回答3: 《JavaScript高级程序设计》第4是由三位著名的前端开发专家编写的JavaScript权威教程。本书介绍了JavaScript的核心概念、语言特性和应用方法,以及一些高级技巧和最佳实践。 本书的第一部分从JavaScript基础语法开始介绍,包括变量声明、数据类型、操作符、语句和函数等方面。第二部分主要介绍JavaScript的面向对象编程,包括原型链、继承和封装等概念。第三部分主要介绍JavaScript的一些高级特性,包括闭包、异步编程、事件和延迟加载等内容。第部分主要介绍了如何使用JavaScript实现一些实际应用,包括调试、性能优化、动态Web页面和跨域请求等方面。 本书内容全面、深入,不仅介绍了JavaScript的基础知识,更重要的是让读者理解了JavaScript的思想和编程风格。编写本书的三位专家都是行业内的大牛,他们的经验和见解非常宝贵,能够帮助读者更好地理解JavaScript。同时,本书的配套网站还提供了很多实例代码和练习题,读者可以通过这些实践来深入理解JavaScript。 总之,《JavaScript高级程序设计》第4是一本非常不错的JavaScript权威教程,无论是初学者还是专业开发者都可以从中受益匪浅。如果你想深入学习JavaScript,这本书绝对值得一读。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值