1.1w字 | 初中级前端 JavaScript 自测清单 - 2 下

简介: 1.1w字 | 初中级前端 JavaScript 自测清单 - 2 下

五、构造函数和 new 运算符

1. 构造函数

构造函数的作用在于 实现可重用的对象创建代码 。 通常,对于构造函数有两个约定:

  • 命名时首字母大写;
  • 只能使用 new 运算符执行。

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 语法如下:

new constructor[([arguments])]

参数如下:

  • constructor一个指定对象实例的类型的类或函数。
  • arguments一个用于被 constructor 调用的参数列表。

2. 简单示例

举个简单示例:

function User (name){
  this.name = name;
  this.isAdmin = false; 
}
const leo = new User('leo');
console.log(leo.name, leo.isAdmin); // "leo" false

3. new 运算符操作过程

当一个函数被使用 new 运算符执行时,它按照以下步骤:

  1. 一个新的空对象被创建并分配给 this
  2. 函数体执行。通常它会修改 this,为其添加新的属性。
  3. 返回 this 的值。

以前面 User 方法为例:

function User(name) {
  // this = {};(隐式创建)
  // 添加属性到 this
  this.name = name;
  this.isAdmin = false;
  // return this;(隐式返回)
}
const leo = new User('leo');
console.log(leo.name, leo.isAdmin); // "leo" false

当我们执行 new User('leo') 时,发生以下事情:

  1. 一个继承自 User.prototype 的新对象被创建;
  2. 使用指定参数调用构造函数 User ,并将 this 绑定到新创建的对象;
  3. 由构造函数返回的对象就是 new 表达式的结果。如果构造函数没有显式返回一个对象,则使用步骤1创建的对象。

需要注意

  1. 一般情况下,构造函数不返回值,但是开发者可以选择主动返回对象,来覆盖正常的对象创建步骤;
  2. new User 等同于 new User() ,只是没有指定参数列表,即 User 不带参数的情况;
let user = new User; // <-- 没有参数
// 等同于
let user = new User();
  1. 任何函数都可以作为构造器,即都可以使用 new 运算符运行。

4. 构造函数中的方法

在构造函数中,也可以将方法绑定到 this 上:

function User (name){
  this.name = name;
  this.isAdmin = false; 
  this.sayHello = function(){
    console.log("hello " + this.name);
  }
}
const leo = new User('leo');
console.log(leo.name, leo.isAdmin); // "leo" false
leo.sayHello(); // "hello leo"

六、可选链 "?."

详细介绍可以查看 《MDN 可选链操作符》

1. 背景介绍

在实际开发中,常常出现下面几种报错情况:

// 1. 对象中不存在指定属性
const leo = {};
console.log(leo.name.toString()); 
// Uncaught TypeError: Cannot read property 'toString' of undefined
// 2. 使用不存在的 DOM 节点属性
const dom = document.getElementById("dom").innerHTML; 
// Uncaught TypeError: Cannot read property 'innerHTML' of null

在可选链 ?. 出现之前,我们会使用短路操作 && 运算符来解决该问题:

const leo = {};
console.log(leo && leo.name && leo.name.toString()); // undefined

这种写法的缺点就是 太麻烦了

2. 可选链介绍

可选链 ?. 是一种 访问嵌套对象属性的防错误方法 。即使中间的属性不存在,也不会出现错误。 如果可选链 ?. 前面部分是 undefined 或者 null,它会停止运算并返回 undefined

语法:

obj?.prop
obj?.[expr]
arr?.[index]
func?.(args)

** 我们改造前面示例代码:

// 1. 对象中不存在指定属性
const leo = {};
console.log(leo?.name?.toString()); 
// undefined
// 2. 使用不存在的 DOM 节点属性
const dom = document?.getElementById("dom")?.innerHTML; 
// undefined

3. 使用注意

可选链虽然好用,但需要注意以下几点:

  1. 不能过度使用可选链

我们应该只将 ?. 使用在一些属性或方法可以不存在的地方,以上面示例代码为例:

const leo = {};
console.log(leo.name?.toString()); 

这样写会更好,因为 leo 对象是必须存在,而 name 属性则可能不存在。

  1. 可选链 ?. 之前的变量必须已声明

在可选链 ?. 之前的变量必须使用 let/const/var 声明,否则会报错:

leo?.name;
// Uncaught ReferenceError: leo is not defined
  1. 可选链不能用于赋值
let object = {};
object?.property = 1; 
// Uncaught SyntaxError: Invalid left-hand side in assignment
  1. 可选链访问数组元素的方法
let arrayItem = arr?.[42];

4. 其他情况:?.() 和 ?.[]

需要说明的是 ?. 是一个特殊的语法结构,而不是一个运算符,它还可以与其 ()[] 一起使用:

4.1 可选链与函数调用 ?.()

?.() 用于调用一个可能不存在的函数,比如:

let user1 = {
  admin() {
    alert("I am admin");
  }
}
let user2 = {};
user1.admin?.(); // I am admin
user2.admin?.();

?.() 会检查它左边的部分:如果 admin 函数存在,那么就调用运行它(对于 user1)。否则(对于 user2)运算停止,没有错误。

4.2 可选链和表达式 ?.[]

?.[] 允许从一个可能不存在的对象上安全地读取属性。

let user1 = {
  firstName: "John"
};
let user2 = null; // 假设,我们不能授权此用户
let key = "firstName";
alert( user1?.[key] ); // John
alert( user2?.[key] ); // undefined
alert( user1?.[key]?.something?.not?.existing); // undefined

5. 可选链 ?. 语法总结

可选链 ?. 语法有三种形式:

  1. obj?.prop —— 如果 obj 存在则返回 obj.prop,否则返回 undefined
  2. obj?.[prop] —— 如果 obj 存在则返回 obj[prop],否则返回 undefined
  3. obj?.method() —— 如果 obj 存在则调用 obj.method(),否则返回 undefined

正如我们所看到的,这些语法形式用起来都很简单直接。?. 检查左边部分是否为 null/undefined,如果不是则继续运算。 ?. 链使我们能够安全地访问嵌套属性。

七、Symbol

规范规定,JavaScript 中对象的属性只能为 字符串类型 或者 Symbol类型 ,毕竟我们也只见过这两种类型。

1. 概念介绍

ES6引入Symbol作为一种新的原始数据类型,表示独一无二的值,主要是为了防止属性名冲突。 ES6之后,JavaScript一共有其中数据类型:SymbolundefinednullBooleanStringNumberObject简单使用

let leo = Symbol();
typeof leo; // "symbol"

Symbol 支持传入参数作为 Symbol 名,方便代码调试: **

let leo = Symbol("leo");

2. 注意事项**

  • Symbol函数不能用new,会报错。

由于Symbol是一个原始类型,不是对象,所以不能添加属性,它是类似于字符串的数据类型。

let leo = new Symbol()
// Uncaught TypeError: Symbol is not leo constructor
  • Symbol都是不相等的,即使参数相同
// 没有参数
let leo1 = Symbol();
let leo2 = Symbol();
leo1 === leo2; // false 
// 有参数
let leo1 = Symbol('leo');
let leo2 = Symbol('leo');
leo1 === leo2; // false
  • Symbol不能与其他类型的值计算,会报错。
let leo = Symbol('hello');
leo + " world!";  // 报错
`${leo} world!`;  // 报错
  • Symbol 不能自动转换为字符串,只能显式转换。
let leo = Symbol('hello');
alert(leo); 
// Uncaught TypeError: Cannot convert a Symbol value to a string
String(leo);    // "Symbol(hello)"
leo.toString(); // "Symbol(hello)"
  • Symbol 可以转换为布尔值,但不能转为数值:
let a1 = Symbol();
Boolean(a1);
!a1;        // false
Number(a1); // TypeError
a1 + 1 ;    // TypeError
  • Symbol 属性不参与 for...in/of 循环。
let id = Symbol("id");
let user = {
  name: "Leo",
  age: 30,
  [id]: 123
};
for (let key in user) console.log(key); // name, age (no symbols)
// 使用 Symbol 任务直接访问
console.log( "Direct: " + user[id] );

3. 字面量中使用 Symbol 作为属性名

在对象字面量中使用 Symbol 作为属性名时,需要使用 方括号[] ),如 [leo]: "leo" 。 好处:防止同名属性,还有防止键被改写或覆盖。

let leo = Symbol();
// 写法1
let user = {};
user[leo] = 'leo';
// 写法2
let user = {
    [leo] : 'leo'
} 
// 写法3
let user = {};
Object.defineProperty(user, leo, {value : 'leo' });
// 3种写法 结果相同
user[leo]; // 'leo'

需要注意 :Symbol作为对象属性名时,不能用点运算符,并且必须放在方括号内。

let leo = Symbol();
let user = {};
// 不能用点运算
user.leo = 'leo';
user[leo] ; // undefined
user['leo'] ; // 'leo'
// 必须放在方括号内
let user = {
    [leo] : function (text){
        console.log(text);
    }
}
user[leo]('leo'); // 'leo'
// 上面等价于 更简洁
let user = {
    [leo](text){
        console.log(text);
    }
}

常常还用于创建一组常量,保证所有值不相等

let user = {};
user.list = {
    AAA: Symbol('Leo'),
    BBB: Symbol('Robin'),
    CCC: Symbol('Pingan')
}

4. 应用:消除魔术字符串

魔术字符串:指代码中多次出现,强耦合的字符串或数值,应该避免,而使用含义清晰的变量代替。

function fun(name){
    if(name == 'leo') {
        console.log('hello');
    }
}
fun('leo');   // 'hello' 为魔术字符串

常使用变量,消除魔术字符串:

let obj = {
    name: 'leo'
};
function fun(name){
    if(name == obj.name){
        console.log('hello');
    }
}
fun(obj.name); // 'hello'

使用Symbol消除强耦合,使得不需关系具体的值:

let obj = {
    name: Symbol()
};
function fun (name){
    if(name == obj.name){
        console.log('hello');
    }
}
fun(obj.name); // 'hello'

5. 属性名遍历

Symbol作为属性名遍历,不出现在for...infor...of循环,也不被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。

let leo = Symbol('leo'), robin = Symbol('robin');
let user = {
    [leo]:'18', [robin]:'28'
}
for(let k of Object.values(user)){console.log(k)}
// 无输出
let user = {};
let leo = Symbol('leo');
Object.defineProperty(user, leo, {value: 'hi'});
for(let k in user){
    console.log(k); // 无输出
}
Object.getOwnPropertyNames(user);   // []
Object.getOwnPropertySymbols(user); // [Symbol(leo)]

Object.getOwnPropertySymbols方法返回一个数组,包含当前对象所有用做属性名的Symbol值。

let user = {};
let leo = Symbol('leo');
let pingan = Symbol('pingan');
user[leo] = 'hi leo';
user[pingan] = 'hi pingan';
let obj = Object.getOwnPropertySymbols(user);
obj; //  [Symbol(leo), Symbol(pingan)]

另外可以使用Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

let user = {
    [Symbol('leo')]: 1,
    age : 2, 
    address : 3,
}
Reflect.ownKeys(user); // ['age', 'address',Symbol('leo')]

由于Symbol值作为名称的属性不被常规方法遍历获取,因此常用于定义对象的一些非私有,且内部使用的方法。

6. Symbol.for()、Symbol.keyFor()

6.1 Symbol.for()

用于重复使用一个Symbol值,接收一个字符串作为参数,若存在用此参数作为名称的Symbol值,返回这个Symbol,否则新建并返回以这个参数为名称的Symbol值。

let leo = Symbol.for('leo');
let pingan = Symbol.for('leo');
leo === pingan;  // true

Symbol()Symbol.for()区别:

Symbol.for('leo') === Symbol.for('leo'); // true
Symbol('leo') === Symbol('leo');         // false

6.2 Symbol.keyFor()

用于返回一个已使用的Symbol类型的key:

let leo = Symbol.for('leo');
Symbol.keyFor(leo);   //  'leo'
let leo = Symbol('leo');
Symbol.keyFor(leo);   //  undefined

7. 内置的Symbol值

ES6提供11个内置的Symbol值,指向语言内部使用的方法:

7.1 Symbol.hasInstance

当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

class P {
    [Symbol.hasInstance](a){
        return a instanceof Array;
    }
}
[1, 2, 3] instanceof new P(); // true

P是一个类,new P()会返回一个实例,该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。

7.2 Symbol.isConcatSpreadable

值为布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

let a = ['aa','bb'];
['cc','dd'].concat(a, 'ee'); 
// ['cc', 'dd', 'aa', 'bb', 'ee']
a[Symbol.isConcatSpreadable]; // undefined
let b = ['aa','bb']; 
b[Symbol.isConcatSpreadable] = false; 
['cc','dd'].concat(b, 'ee'); 
// ['cc', 'dd',[ 'aa', 'bb'], 'ee']

7.3 Symbol.species

指向一个构造函数,在创建衍生对象时会使用,使用时需要用get取值器。

class P extends Array {
    static get [Symbol.species](){
        return this;
    }
}

解决下面问题:

// 问题:  b应该是 Array 的实例,实际上是 P 的实例
class P extends Array{}
let a = new P(1,2,3);
let b = a.map(x => x);
b instanceof Array; // true
b instanceof P; // true
// 解决:  通过使用 Symbol.species
class P extends Array {
  static get [Symbol.species]() { return Array; }
}
let a = new P();
let b = a.map(x => x);
b instanceof P;     // false
b instanceof Array; // true

7.4 Symbol.match

当执行str.match(myObject),传入的属性存在时会调用,并返回该方法的返回值。

class P {
    [Symbol.match](string){
        return 'hello world'.indexOf(string);
    }
}
'h'.match(new P());   // 0

7.5 Symbol.replace

当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

let a = {};
a[Symbol.replace] = (...s) => console.log(s);
'Hello'.replace(a , 'World') // ["Hello", "World"]

7.6 Symbol.hasInstance

当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

class P {
    constructor(val) {
        this.val = val;
    }
    [Symbol.search](s){
        return s.indexOf(this.val);
    }
}
'hileo'.search(new P('leo')); // 2

7.7 Symbol.split

当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

// 重新定义了字符串对象的split方法的行为
class P {
    constructor(val) {
        this.val = val;
    }
    [Symbol.split](s) {
        let i = s.indexOf(this.val);
        if(i == -1) return s;
        return [
            s.substr(0, i),
            s.substr(i + this.val.length)
        ]
    }
}
'helloworld'.split(new P('hello')); // ["hello", ""]
'helloworld'.split(new P('world')); // ["", "world"] 
'helloworld'.split(new P('leo'));   // "helloworld"

7.8 Symbol.iterator

对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器。

class P {
    *[Symbol.interator]() {
        let i = 0;
        while(this[i] !== undefined ) {
            yield this[i];
            ++i;
        }
    }
}
let a = new P();
a[0] = 1;
a[1] = 2;
for (let k of a){
    console.log(k);
}

7.9.Symbol.toPrimitive

该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。调用时,需要接收一个字符串参数,表示当前运算模式,运算模式有:

  • Number : 此时需要转换成数值
  • String : 此时需要转换成字符串
  • Default : 此时可以转换成数值或字符串
let obj = {
  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case 'number':
        return 123;
      case 'string':
        return 'str';
      case 'default':
        return 'default';
      default:
        throw new Error();
     }
   }
};
2 * obj // 246
3 + obj // '3default'
obj == 'default' // true
String(obj) // 'str'

7.10 Symbol.toStringTag

在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object]或[object Array]object后面的那个字符串。

// 例一
({[Symbol.toStringTag]: 'Foo'}.toString())
// "[object Foo]"
// 例二
class Collection {
  get [Symbol.toStringTag]() {
    return 'xxx';
  }
}
let x = new Collection();
Object.prototype.toString.call(x) // "[object xxx]"

7.11 Symbol.unscopables

该对象指定了使用with关键字时,哪些属性会被with环境排除。

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

上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。

八、原始值转换

前面复习到字符串、数值、布尔值等的转换,但是没有讲到对象的转换规则,这部分就一起看看:。 需要记住几个规则:

  1. 所有对象在布尔上下文中都为 true ,并且不存在转换为布尔值的操作,只有字符串和数值转换有。
  2. 数值转换发生在对象相减或应用数学函数时。如 Date 对象可以相减,如 date1 - date2 结果为两个时间的差值。
  3. 在字符串转换,通常出现在如 alert(obj) 这种形式。

当然我们可以使用特殊的对象方法,对字符串和数值转换进行微调。下面介绍三个类型(hint)转换情况:

1. object to string

对象到字符串的转换,当我们对期望一个字符串的对象执行操作时,如 “alert”:

// 输出
alert(obj);
// 将对象作为属性键
anotherObj[obj] = 123;

2. object to number

对象到数字的转换,例如当我们进行数学运算时:

// 显式转换
let num = Number(obj);
// 数学运算(除了二进制加法)
let n = +obj; // 一元加法
let delta = date1 - date2;
// 小于/大于的比较
let greater = user1 > user2;

3. object to default

少数情况下,当运算符“不确定”期望值类型时。 例如,二进制加法 + 可用于字符串(连接),也可以用于数字(相加),所以字符串和数字这两种类型都可以。因此,当二元加法得到对象类型的参数时,它将依据 "default" 来对其进行转换。 此外,如果对象被用于与字符串、数字或 symbol 进行 == 比较,这时到底应该进行哪种转换也不是很明确,因此使用 "default"

// 二元加法使用默认 hint
let total = obj1 + obj2;
// obj == number 使用默认 hint
if (user == 1) { ... };

4. 类型转换算法

为了进行转换,JavaScript 尝试查找并调用三个对象方法:

  1. 调用 obj[Symbol.toPrimitive](hint) —— 带有 symbol 键 Symbol.toPrimitive(系统 symbol)的方法,如果这个方法存在的话,
  2. 否则,如果 hint 是 "string" —— 尝试 obj.toString()obj.valueOf(),无论哪个存在。
  3. 否则,如果 hint 是 "number""default" —— 尝试 obj.valueOf()obj.toString(),无论哪个存在。

5. Symbol.toPrimitive

详细介绍可阅读《MDN | Symbol.toPrimitive》Symbol.toPrimitive 是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数。 简单示例介绍:

let user = {
  name: "Leo",
  money: 9999,
  [Symbol.toPrimitive](hint) {
    console.log(`hint: ${hint}`);
    return hint == "string" ? `{name: "${this.name}"}` : this.money;
  }
};
alert(user);     // 控制台:hint: string 弹框:{name: "John"}
alert(+user);    // 控制台:hint: number 弹框:9999
alert(user + 1); // 控制台:hint: default 弹框:10000

6. toString/valueOf

toString / valueOf 是两个比较早期的实现转换的方法。当没有 Symbol.toPrimitive ,那么 JavaScript 将尝试找到它们,并且按照下面的顺序进行尝试:

  • 对于 “string” hint,toString -> valueOf
  • 其他情况,valueOf -> toString

这两个方法必须返回一个原始值。如果 toStringvalueOf 返回了一个对象,那么返回值会被忽略。默认情况下,普通对象具有 toStringvalueOf 方法:

  • toString 方法返回一个字符串 "[object Object]"
  • valueOf 方法返回对象自身。

简单示例介绍:

const user = {name: "Leo"};
alert(user); // [object Object]
alert(user.valueOf() === user); // true

我们也可以结合 toString / valueOf  实现前面第 5 点介绍的 user 对象:

let user = {
  name: "Leo",
  money: 9999,
  // 对于 hint="string"
  toString() {
    return `{name: "${this.name}"}`;
  },
  // 对于 hint="number" 或 "default"
  valueOf() {
    return this.money;
  }
};
alert(user);     // 控制台:hint: string 弹框:{name: "John"}
alert(+user);    // 控制台:hint: number 弹框:9999
alert(user + 1); // 控制台:hint: default 弹框:10000

总结

本文作为《初中级前端 JavaScript 自测清单》第二部分,介绍的内容以 JavaScript 对象为主,其中有让我眼前一亮的知识点,如 Symbol.toPrimitive 方法。我也希望这个清单能帮助大家自测自己的 JavaScript 水平并查缺补漏,温故知新。

目录
相关文章
|
1天前
|
JavaScript 前端开发 开发者
【Web 前端】什么是JS变量提升?
【5月更文挑战第1天】【Web 前端】什么是JS变量提升?
【Web 前端】什么是JS变量提升?
|
2天前
|
缓存 前端开发 JavaScript
【JavaScript 技术专栏】JavaScript 前端路由实现原理
【4月更文挑战第30天】本文探讨了JavaScript前端路由在SPA中的重要性,阐述了其基本原理和实现方式,包括Hash路由和History路由。前端路由通过监听URL变化、匹配规则来动态切换内容,提升用户体验和交互性。同时,文章也提到了面临的SEO和页面缓存挑战,并通过电商应用案例分析实际应用。理解并掌握前端路由能助开发者打造更流畅的单页应用。
|
3天前
|
前端开发 JavaScript 数据安全/隐私保护
前端javascript的DOM对象操作技巧,全场景解析(二)
前端javascript的DOM对象操作技巧,全场景解析(二)
|
3天前
|
移动开发 缓存 JavaScript
前端javascript的DOM对象操作技巧,全场景解析(一)
前端javascript的DOM对象操作技巧,全场景解析(一)
|
3天前
|
缓存 编解码 自然语言处理
前端javascript的BOM对象知识精讲
前端javascript的BOM对象知识精讲
|
3天前
|
JavaScript 前端开发 开发者
【Web 前端】JS模块化有哪些?
【4月更文挑战第22天】【Web 前端】JS模块化有哪些?
|
3天前
|
前端开发 JavaScript
【Web 前端】 js中call、apply、bind有什么区别?
【4月更文挑战第22天】【Web 前端】 js中call、apply、bind有什么区别?
【Web 前端】 js中call、apply、bind有什么区别?
|
3天前
|
前端开发 JavaScript 索引
【Web 前端】JS的几种具体异常类型(报错)
【4月更文挑战第22天】【Web 前端】JS的几种具体异常类型(报错)
|
27天前
|
前端开发 JavaScript 网络协议
前端最常见的JS面试题大全
【4月更文挑战第3天】前端最常见的JS面试题大全
48 5
|
4月前
|
存储 缓存 前端开发
2023前端面试题总结:JavaScript篇完整版(二)
2023前端面试题总结:JavaScript篇完整版(二)