ES6——Symbol详解

Symbol详解

1、概述

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

ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第7种数据类型,前6种分别是:Undefined、Null、布尔值(Boolean)、字符串(String)、数值(Number)和对象(Object)。

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

js 复制代码
let s = Symbol();
console.log(typeof s);//symbol

上面的代码中,变量s就是一个独一无二的值。typeof运算符的结果表明变量s是Symbol数据类型,而不是字符串之类的其他类型。

注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

Symbol函数可以接受一个字符串作为参数,表示对Symbol实例的描述,主要是为了在控制台显示或转为字符串时比较容易区分。

js 复制代码
let s1 = Symbol('foo');
let s2 = Symbol('bar');

console.log(s1);//Symbol(foo)
console.log(s2);//Symbol(bar)

console.log(s1.toString());//Symbol(foo)
console.log(s2.toString());//Symbol(bar)

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

注意,Symbol函数的参数只表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。

js 复制代码
//没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
console.log(s1 === s2);//false

//有参数的情况
let s3 = Symbol("foo");
let s4 = Symbol("foo");
console.log(s3 === s4);//false

上面的代码中,s1和s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。Symbol值不能与其他类型的值进行运算,否则会报错。

js 复制代码
let sym = Symbol("My symbol");

"your symbol is " + sym;//Error

`your symbol is ${sym}`;//Error

但是,Symbol值可以显式转为字符串。

js 复制代码
let sym = Symbol('My symbol');

console.log(String(sym));//Symbol(My symbol)
console.log(sym.toString());//Symbol(My symbol)

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

js 复制代码
let sym = Symbol();
Boolean(sym);//true
!sym //false

if(sym) {
    //...
}

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

2、作为属性名的Symbol

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

js 复制代码
let mySymbol = Symbol();

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

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

//第三种写法
let a = {};
Object.defineProperty(a, mySymbol, {value: 'Hello!'});

//以上写法都得到同样的结构
a[mySymbol]//'hello!'

上面的代码通过方括号结构和Object.defineProperty将对象的属性名指定为一个Symbol值。

注意,Symbol值作为对象属性名时不能使用点运算符。

js 复制代码
let mySymbol = Symbol();
let a = {};

a.mySymbol = 'Hello!';
a[mySymbol]//undefined
a['mySymbol']//'Hello!'

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

同理,在对象的内部,使用Symbol值定义属性时,Symbol值必须放在方括号中。

js 复制代码
let s = Symbol();

let obj = {
    [s]: function(arg) {}
};
obj[s](123);

上面的代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的Symbol值。

采用增强的对象写法,上面的obj对象可以写得更简洁一些。

js 复制代码
let obj = {
    [s](arg) {}
};

Symbol类型还可用于定义一组常量,保证这组常量的值都是不相等的。

js 复制代码
log.levels = {
    DEBUG: Symbol('debug'),
    INFO: Symbol('info'),
    WARN:Symbol('warn')
};
log(log.levels.DEBUG, 'debug message');
log(log.levels.INFO, 'info message');

下面是另外一个例子。

js 复制代码
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值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。

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

3、消除魔术字符串

魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或数值。风格良好的代码,应该尽量消除魔术字符串,而由含义清晰的变量代替。

js 复制代码
function getArea(shape, options) {
    let area = 0;
    switch(shape) {
        case 'Triangle': //魔术字符串
            area = .5 * options.width * options.height;
            break;
    }
    return area;
}

getArea('Triangle', {width: 100, height: 100});//魔术字符串

上面的代码中,字符串ˈTriangleˈ就是一个魔术字符串。它多次出现,与代码形成"强耦合"​,不利于将来的修改和维护。

常用的消除魔术字符串的方法,就是把它写成一个变量。

js 复制代码
let shapeType = {
    triangle: 'Triangle'
};
function getArea(shape, options) {
    let area = 0;
    switch(shape) {
        case shapeType.triangle:
            area = .5 * options.width *options.height;
            break;
    }
    return area;
}
getArea(shapeType.triangle, {width: 100, height: 100});

上面的代码中,我们把ˈTriangleˈ写成shapeType对象的triangle属性,这样就消除了强耦合。如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用Symbol值。

js 复制代码
const shapeType = {
	triangle: Symbol()
};

上面的代码中,除了将shapeType.triangle的值设为一个Symbol,其他地方都不用修改。

4、属性名的遍历

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

Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的Symbol值。

js 复制代码
let obj = {};
let a = Symbol('a');
let b = Symbol.for('b');

obj[a] = 'Hello';
obj[b] = 'World';

let objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols);
//[ Symbol(a), Symbol(b) ]

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

js 复制代码
let obj = {};
let foo = Symbol("foo");
Object.defineProperty(obj, foo, {
    value: 'foobar'
});
for(let i in obj) {
    console.log(i);//无输出
}
console.log(Object.getOwnPropertyNames(obj));//[]
console.log(Object.getOwnPropertySymbols(obj));//[ Symbol(foo) ]

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

另一个新的API------Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和Symbol键名。

js 复制代码
let obj = {
    [Symbol('my_key')]: 1,
    enum: 2,
    nonEnum: 3
};
console.log(Reflect.ownKeys(obj));
//[ 'enum', 'nonEnum', Symbol(my_key) ]

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

js 复制代码
let size = Symbol('size');

class Collection {
    constructor(){
        this[size] = 0;
    }

    add(item) {
        this[this[size]] = item;
        this[size]++;
    }

    static sizeOf(instance) {
        return instance[size];
    }
}

let x = new Collection();
console.log(Collection.sizeOf(x));//0

x.add('foo')
console.log(Collection.sizeOf(x));//1

console.log(Object.keys(x));//[ '0' ]
console.log(Object.getOwnPropertyNames(x));//[ '0' ]
console.log(Object.getOwnPropertySymbols(x));//[ Symbol(size) ]

上面的代码中,对象x的size属性是一个Symbol值,所以Object.keys(x)、Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。

5、Symbol.for(),Symbol.keyFor()

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

js 复制代码
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
console.log(s1 === s2);//true

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

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

js 复制代码
console.log(Symbol.for('bar') === Symbol.for('bar'));//true
console.log(Symbol('bar') === Symbol('bar'));//false

上面的代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。

Symbol.keyFor方法返回一个已登记的Symbol类型值的key。

js 复制代码
let s1 = Symbol.for('foo');
console.log(Symbol.keyFor(s1));//foo

let s2 = Symbol('foo');
console.log(Symbol.keyFor(s2));//undefined

上面的代码中,变量s2属于未登记的Symbol值,所以返回undefined。

需要注意的是,Symbol.for为Symbol值登记的名字是全局环境的,可以在不同的iframe或service worker中取到同一个值。

js 复制代码
iframe = document.createElement('iframe');
iframe.src = String(window.location);
document.body.appendChild(iframe);

iframe.contentWindow.Symbol.for('foo') === Symbol.for('foo');
//true

上面的代码中,iframe窗口生成的Symbol值可以在主页面得到。

6、内置的Symbol值

除了定义自己使用的Symbol值外,ES6还提供了11个内置的Symbol值,指向语言内部使用的方法。

6.1、Symbol.hasInstance

对象的Symbol.hasInstance属性指向一个内部方法,对象使用instanceof运算符时会调用这个方法,判断该对象是否为某个构造函数的实例。比如,foo instanceof Foo在语言内部实际调用的是FooSymbol.hasInstance

Symbol.hasInstance的函数签名:

js 复制代码
static [Symbol.hasInstance](value: any): boolean
  • 必须是 静态方法
  • 接收一个参数:instance(即 instanceof左边的值)
  • 返回 true / false
js 复制代码
class MyClass {
    static [Symbol.hasInstance](foo) {
        console.log('被调用...');
        return false;
    }
}
let o = new MyClass();
console.log(o instanceof MyClass);//false

应用示例:

  1. 包装类型 / 抽象类:适合用于"逻辑上的类型",而不是具体构造函数创建的对象。
js 复制代码
class NumberLike {
  static [Symbol.hasInstance](value) {
    return typeof value === 'number' || value instanceof Number;
  }
}

console.log(123 instanceof NumberLike);   // true
console.log(new Number(123) instanceof NumberLike); // true
  1. 接口模拟(Duck Typing):不关心原型链,只关心"有没有某种能力"。
js 复制代码
class Flyer {
  static [Symbol.hasInstance](obj) {
    return typeof obj.fly === 'function';
  }
}

const bird = { fly() {} };
const nobird = {};
console.log(bird instanceof Flyer); // true
console.log(nobird instanceof Flyer); // false
  1. 兼容旧代码 / 多实现统一判断
js 复制代码
class PromiseLike {
  static [Symbol.hasInstance](obj) {
    return obj && typeof obj.then === 'function';
  }
}

console.log(Promise.resolve() instanceof PromiseLike); // true

6.2、Symbol.isConcatSpreadabIe

对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象使用Array.prototype.concat()时是否可以展开。

Symbol.isConcatSpreadable用来标识:当该对象作为参数传入 Array.prototype.concat时,是否应当被"展开(spread)"成数组元素。

简单说就是:

  • true→ 像数组一样被展开
  • false / 未设置→ 作为一个整体追加
js 复制代码
let arr1 = ['c', 'd'];
console.log(['a', 'b'].concat(arr1, 'e'));//[ 'a', 'b', 'c', 'd', 'e' ]

let arr2 = ['c', 'd'];
arr2[Symbol.isConcatSpreadable] = false;
console.log(['a', 'b'].concat(arr2, 'e'));
// [
//   'a',
//   'b',
//   [ 'c', 'd', [Symbol(Symbol.isConcatSpreadable)]: false ],
//   'e'
// ]

上面的代码说明,数组的Symbol.isConcatSpreadable属性默认为true,表示可以展开。

类似数组的对象也可以展开,但其Symbol.isConcatSpreadable属性默认为false,必须手动打开。

js 复制代码
let obj = {length: 2, 0: 'c', 1: 'd'};
console.log(['a', 'b'].concat(obj, 'e'))
//[ 'a', 'b', { '0': 'c', '1': 'd', length: 2 }, 'e' ]

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

对于一个类而言,Symbol.isConcatSpreadable属性必须写成一个返回布尔值的方法。

js 复制代码
class A1 extends Array {
    [Symbol.isConcatSpreadable] = true;
}
class A2 extends Array {
    [Symbol.isConcatSpreadable] = false;
}
let a1 = new A1();
a1[0] = 3;
a1[1] = 4;
let a2 = new A2();
a2[0] = 5;
a2[1] = 6;
console.log([1, 2].concat(a1).concat(a2));
// [
//   1,
//   2,
//   3,
//   4,
//   A2(2) [ 5, 6, [Symbol(Symbol.isConcatSpreadable)]: false ]
// ]

上面的代码中,类A1是可扩展的,类A2是不可扩展的,所以使用concat时有不一样的结果。

6.3、Symbol.species

对象的Symbol.species属性指向一个方法,对象作为构造函数创造实例时会调用这个方法。即如果this.constructor[Symbol.species]存在,就会使用这个属性作为构造函数来创造新的实例对象。

Symbol.species用来指定:当内置方法返回一个新的实例时,应该使用哪个构造函数。

常见于:

  • Array.prototype.map
  • Array.prototype.filter
  • Promise.prototype.then
  • TypedArray.prototype.slice

Symbol.species属性默认的读取器如下。

js 复制代码
static get [Symbol.species]() {
    return this;
}

为什么需要Symbol.species

假设你创建了一个 自定义数组子类:

js 复制代码
class MyArray extends Array {}

然后你这样做:

js 复制代码
const a = new MyArray(1, 2, 3);
const b = a.map(x => x * 2);

问题来了:b应该是 Array还是 MyArray?

默认行为是:

js 复制代码
b instanceof MyArray; // true

但在很多场景下(尤其是安全/性能考虑),你可能希望:

js 复制代码
b instanceof Array; // true

Symbol.species就是用来解决这个问题的。

基本用法:

js 复制代码
//默认行为(不使用 species)
class MyArray extends Array {}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);

console.log(b.constructor === MyArray); // true

使用 Symbol.species改变返回类型

js 复制代码
class MyArray extends Array {
  static get [Symbol.species]() {
    return Array;
  }
}

const a = new MyArray(1, 2, 3);
const b = a.map(x => x);

console.log(b instanceof MyArray); // false
console.log(b instanceof Array);  // true

现在所有返回新实例的方法,都会使用 Array而不是 MyArray。

6.4、Symbol.match

对象的Symbol.match属性指向一个函数,当执行str.match(myObject)时,如果该属性存在,会调用它返回该方法的返回值。

它是 JavaScript 中用于定义对象是否可被当作正则表达式使用(尤其是在 String.prototype.match中)的内置 Symbol。

Symbol.match决定一个对象在被传给 String.prototype.match时,是否以及如何作为正则使用。

js 复制代码
//如果 obj定义了 Symbol.match,就会调用它,而不是当作普通正则。
str.match(obj)
js 复制代码
String.prototype.match(regexp);
//等价于
regexp[Symbol.match(this)];
js 复制代码
class MyMatch {
    [Symbol.match](string) {
        return 'hello world'.indexOf(string);
    }
}

console.log('e'.match(new MyMatch()));//1

6.5、Symbol.search

对象的Symbol.search属性指向一个方法,当对象被String.prototype.search方法调用时会返回该方法的返回值。

它是 JavaScript 中用于自定义对象在 String.prototype.search中如何参与查找位置的内置 Symbol。

Symbol.search决定一个对象在被传给 String.prototype.search时,如何进行"索引位置查找"。

js 复制代码
//如果 obj定义了 Symbol.search,就会调用它,而不是当作普通正则。
str.search(obj)
js 复制代码
String.prototype.search(regexp);
//等价于
regexp[Symbol.search](this);
js 复制代码
class MySearch {
    constructor(value) {
        this.value = value;
    }

    [Symbol.search](string) {
        return string.indexOf(this.value);
    }
}

console.log('foobar'.search(new MySearch('foo')));//0

6.6、Symbol.spIit

对象的Symbol.split属性指向一个方法,当对象被String.prototype.split方法调用时会返回该方法的返回值。

它是 JavaScript 中用于自定义对象在 String.prototype.split中如何参与字符串分割的内置 Symbol。

Symbol.split决定一个对象在被传给 String.prototype.split时,如何进行字符串分割。

js 复制代码
//如果 obj定义了 Symbol.split,就会调用它,而不是当作普通分隔符。
str.split(obj)
js 复制代码
String.prototype.split(separator, limit);
//等价于
regexp[Symbol.split](this, limit);
js 复制代码
//按长度分割
class MySplit {
  [Symbol.split](str, limit) {
    const result = [];
    for (let i = 0; i < str.length; i += limit) {
      result.push(str.slice(i, i + limit));
    }
    return result;
  }
};

console.log('abcdef'.split(new MySplit(), 2));
// ['ab', 'cd', 'ef']

6.7、Symbol.iterator

对象的Symbol.iterator属性指向其默认遍历器方法,即对象在进行for...of循环时会调用这个方法,返回该对象的默认遍历器。

它是 JavaScript 中最基础、最重要的内置 Symbol 之一,也是理解 迭代协议、for...of、解构、展开运算符​ 的关键。

Symbol.iterator是一个方法,返回一个迭代器(Iterator)对象,用于定义对象的"可迭代行为"。只要对象实现了它:

js 复制代码
for (const item of obj) { ... }

就能工作。

js 复制代码
class Collection {
    *[Symbol.iterator]() {
        let i = 0;
        while(this[i] !== undefined) {
            yield this[i];
            ++i;
        }
    }
}

let myCollection = new Collection();
myCollection[0] = 1;
myCollection[1] = 2;
for(let value of myCollection) {
    console.log(value);
}
//1
//2
js 复制代码
class Range {
  constructor(start, end) {
    this.start = start;
    this.end = end;
  }

  [Symbol.iterator]() {
    let current = this.start;
    const end = this.end;

    return {
      next() {
        if (current <= end) {
          return { value: current++, done: false };
        }
        return { value: undefined, done: true };
      }
    };
  }
}

for (const n of new Range(1, 3)) {
  console.log(n);
}
// 1
// 2
// 3

6.8、Symbol.toPrimitive

对象的Symbol.toPrimitive属性指向一个方法,对象被转为原始类型的值时会调用这个方法,返回该对象对应的原始类型值。

它是 JavaScript 中用于控制对象如何被转换为原始值(primitive)的内置 Symbol,是类型转换机制里的"终极钩子"。

Symbol.toPrimitive决定了一个对象在被强制转换为原始值(数字、字符串、布尔等)时,返回什么值。

Symbol.toPrimitive决定了一个对象在被强制转换为原始值(数字、字符串、布尔等)时,返回什么值。

  • Number:该场合需要转成数值。
  • String:该场合需要转成字符串。
  • Default:该场合可以转成数值,也可以转成字符串。
js 复制代码
let obj = {
    [Symbol.toPrimitive](hint) {
        switch(hint) {
            case 'number':
                return 123;
            case 'string':
                return 'str';
            case 'default':
                return 'default';
            default:
                throw new Error();
        }
    }
};

console.log(2 * obj);//246
console.log(3 + obj);//3default
console.log(obj === 'default');//false
console.log(String(obj));//str

6.9、Symbol.toStringTag

对象的Symbol.toStringTag属性指向一个方法,在对象上调用Object.prototype.toString方法时,如果这个属性存在,其返回值会出现在toString方法返回的字符串中,表示对象的类型。也就是说,这个属性可用于定制[objectObject]或[object Array]中object后面的字符串。

它是 JavaScript 中用于定制对象在 Object.prototype.toString.call()中返回的标签的内置 Symbol,也是判断对象"类型名"的关键机制。

Symbol.toStringTag决定了 Object.prototype.toString.call(obj)返回的 [object XXXX]中的 XXXX是什么。

js 复制代码
class A {
    [Symbol.toStringTag] = 'Foo'
}

console.log(new A().toString());//[object Foo]
console.log(Object.prototype.toString.call(new A()));//[object Foo]

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ˈ

6.10、Symbol.unscopabIes

对象的Symbol.unscopables属性指向一个对象,指定了使用with关键字时哪些属性会被with环境排除。

Symbol.unscopables用来标记:在 with语句中,哪些属性不应该被当作"作用域变量"暴露出来。

js 复制代码
with (obj) {
  // 哪些属性可以直接访问?
}

历史背景:

早期 JS 中:

js 复制代码
const obj = { a: 1, b: 2 };

with (obj) {
  console.log(a); // 1
  console.log(b); // 2
}

问题来了:

js 复制代码
const obj = { toString: 'hello' };

with (obj) {
  toString(); 
  // ❌ 调用的是 obj.toString,而不是全局的
}
  • 破坏了作用域直觉
  • 严重破坏性能和可维护性

ES6 开始:

  • with被严格模式禁用
  • 新增 Symbol.unscopables作为"补救机制"
js 复制代码
//没有unscapables时
class MyClass {
    foo() {
        return 1;
    }
}

let foo = function() {
    return 2;
}

with (MyClass.prototype) {
    foo();//1
}

//有unscopables时
class MyClass {
    foo() {
        return 1;
    }
    get [Symbol.unscopables]() {
        return {foo: true};
    }
}
let foo = function() {
    return 2;
}

with (MyClass.prototype) {
    foo();//2
}
相关推荐
星辰徐哥2 小时前
C语言运算符的优先级与结合性详解
c语言·开发语言
HZ·湘怡2 小时前
顺序表 2 续集 c 实现增删查改
c语言·开发语言·顺序表
han_hanker2 小时前
js 加减乘除精度问题2
开发语言·javascript·ecmascript
红目香薰2 小时前
Ascend C 算子:Sigmoid 函数原理深入解析与工程化构建及验证
c语言·开发语言·华为·华为云·昇腾·cann·modelarts
OTWOL2 小时前
C语言操作符终极揭秘:表达式求值秘籍
c语言·开发语言·c++
无巧不成书02182 小时前
Java 21 LTS 高级特性零基础通关:静态导入、项目目录规范、泛型全实战
java·开发语言·标准目录结构·泛型原理·类型安全实现
张np2 小时前
java框架和http调用接口的区别
java·开发语言·http
李日灐2 小时前
【优选算法3】二分查找经典算法面试题
开发语言·c++·后端·算法·面试·二分查找·双指针
ldybk2 小时前
教学vue
前端·javascript·vue.js