2024年Web前端最新初中级前端 JavaScript 自测清单 - 2(2),面试的那些事

最后

我可以将最近整理的前端面试题分享出来,其中包含HTML、CSS、JavaScript、服务端与网络、Vue、浏览器、数据结构与算法等等,还在持续整理更新中,希望大家都能找到心仪的工作。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

篇幅有限,仅展示部分截图:

let user = { name: “leo”, age: 18};

Object.keys(user); // [“name”, “age”]

2. Object.values()

返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历属性的键值。

let user = { name: “leo”, age: 18};

Object.values(user); // [“leo”, 18]

如果参数不是对象,则返回空数组:

Object.values(10);   // []

Object.values(true); // []

3. Object.entries()

返回一个数组,成员是参数对象自身的(不含继承的)所有**「可遍历属性」**的键值对数组。

let user = { name: “leo”, age: 18};

Object.entries(user);

// [[“name”,“leo”],[“age”,18]]

手动实现Object.entries()方法:

// Generator函数实现:

function* entries(obj){

for (let k of Object.keys(obj)){

yield [k ,obj[k]];

}

}

// 非Generator函数实现:

function entries (obj){

let arr = [];

for(let k of Object.keys(obj)){

arr.push([k, obj[k]]);

}

return arr;

}

4. Object.getOwnPropertyNames(Obj)

该方法返回一个数组,它包含了对象 Obj 所有拥有的属性(「无论是否可枚举」)的名称。

let user = { name: “leo”, age: 18};

Object.getOwnPropertyNames(user);

// [“name”, “age”]

二、对象拷贝

======

参考文章《搞不懂JS中赋值·浅拷贝·深拷贝的请看这里》

1. 赋值操作


首先回顾下基本数据类型和引用数据类型:

  • 基本类型

概念:基本类型值在内存中占据固定大小,保存在栈内存中(不包含闭包中的变量)。常见包括:undefined,null,Boolean,String,Number,Symbol

  • 引用类型

概念:引用类型的值是对象,保存在堆内存中。而栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址(引用),引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。常见包括:Object,Array,Date,Function,RegExp等

1.1 基本数据类型赋值

在栈内存中的数据发生数据变化的时候,系统会自动为新的变量分配一个新的之值在栈内存中,两个变量相互独立,互不影响的。

let user  = “leo”;

let user1 = user;

user1 = “pingan”;

console.log(user);  // “leo”

console.log(user1); // “pingan”

1.2 引用数据类型赋值

在 JavaScript 中,变量不存储对象本身,而是存储其“内存中的地址”,换句话说就是存储对其的“引用”。如下面 leo  变量只是保存对user 对象对应引用:

let user = { name: “leo”, age: 18};

let leo  = user;

其他变量也可以引用 user 对象:

let leo1 = user;

let leo2 = user;

但是由于变量保存的是引用,所以当我们修改变量 leo \ leo1 \ leo2 这些值时,「也会改动到引用对象」 user ,但当 user 修改,则其他引用该对象的变量,值都会发生变化:

leo.name = “pingan”;

console.log(leo);   // {name: “pingan”, age: 18}

console.log(leo1);  // {name: “pingan”, age: 18}

console.log(leo2);  // {name: “pingan”, age: 18}

console.log(user);  // {name: “pingan”, age: 18}

user.name = “pingan8787”;

console.log(leo);   // {name: “pingan8787”, age: 18}

console.log(leo1);  // {name: “pingan8787”, age: 18}

console.log(leo2);  // {name: “pingan8787”, age: 18}

console.log(user);  // {name: “pingan8787”, age: 18}

这个过程中涉及变量地址指针指向问题,这里暂时不展开讨论,有兴趣的朋友可以网上查阅相关资料。

2. 对象比较


当两个变量引用同一个对象时,它们无论是 == 还是 === 都会返回 true

let user = { name: “leo”, age: 18};

let leo  = user;

let leo1 = user;

leo ==  leo1;   // true

leo === leo1;   // true

leo ==  user;   // true

leo === user;   // true

但如果两个变量是空对象 {} ,则不相等:

let leo1 = {};

let leo2 = {};

leo1 ==  leo2;  // false

leo1 === leo2;  // false

3. 浅拷贝


3.1 概念

概念:「新的对象复制已有对象中非对象属性的值和对象属性的引用」。也可以理解为:「一个新的对象直接拷贝已存在的对象的对象属性的引用」,即浅拷贝。

浅拷贝**「只对第一层属性进行了拷贝」**,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,但是如果第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。

通过示例代码演示没有使用浅拷贝场景:

// 示例1 对象原始拷贝

let user = { name: “leo”, skill: { JavaScript: 90, CSS: 80}};

let leo = user;

leo.name = “leo1”;

leo.skill.CSS = 90;

console.log(leo.name);      // “leo1”

console.log(user.name);     // “leo1”

console.log(leo.skill.CSS); // 90

console.log(user.skill.CSS);// 90

// 示例2 数组原始拷贝

let user = [“leo”, “pingan”, {name: “pingan8787”}];

let leo  = user;

leo[0] = “pingan888”;

leo[2][“name”] = “pingan999”;

console.log(leo[0]);          // “pingan888”

console.log(user[0]);         // “pingan888”

console.log(leo[2][“name”]);  // “pingan999”

console.log(user[2][“name”]); // “pingan999”

从上面示例代码可以看出:由于对象被直接拷贝,相当于拷贝 「引用数据类型」 ,所以在新对象修改任何值时,都会改动到源数据。

接下来实现浅拷贝,对比以下。

3.2 实现浅拷贝

1. Object.assign()

语法:Object.assign(target, ...sources)ES6中拷贝对象的方法,接受的第一个参数是拷贝的目标target,剩下的参数是拷贝的源对象sources(可以是多个)。详细介绍,可以阅读文档《MDN Object.assign》。

// 示例1 对象浅拷贝

let user = { name: “leo”, skill: { JavaScript: 90, CSS: 80}};

let leo = Object.assign({}, user);

leo.name = “leo1”;

leo.skill.CSS = 90;

console.log(leo.name);      // “leo1” ⚠️ 差异!

console.log(user.name);     // “leo”  ⚠️ 差异!

console.log(leo.skill.CSS); // 90

console.log(user.skill.CSS);// 90

// 示例2 数组深拷贝

let user = [“leo”, “pingan”, {name: “pingan8787”}];

let leo  = user;

leo[0] = “pingan888”;

leo[2][“name”] = “pingan999”;

console.log(leo[0]);          // “pingan888”  ⚠️ 差异!

console.log(user[0]);         // “leo”        ⚠️ 差异!

console.log(leo[2][“name”]);  // “pingan999”

console.log(user[2][“name”]); // “pingan999”

从打印结果可以看出,浅拷贝只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。

Object.assign() 使用注意:

  • 只拷贝源对象的自身属性(不拷贝继承属性);

  • 不会拷贝对象不可枚举的属性;

  • 属性名为Symbol 值的属性,可以被Object.assign拷贝;

  • undefinednull无法转成对象,它们不能作为Object.assign参数,但是可以作为源对象。

Object.assign(undefined); // 报错

Object.assign(null);      // 报错

Object.assign({}, undefined); // {}

Object.assign({}, null);      // {}

let user = {name: “leo”};

Object.assign(user, undefined) === user; // true

Object.assign(user, null)      === user; // true

2. Array.prototype.slice()

语法:arr.slice([begin[, end]])``slice() 方法返回一个新的数组对象,这一对象是一个由 beginend 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。详细介绍,可以阅读文档《MDN Array slice》。

// 示例 数组深拷贝

let user = [“leo”, “pingan”, {name: “pingan8787”}];

let leo  = Array.prototype.slice.call(user);

leo[0] = “pingan888”;

leo[2][“name”] = “pingan999”;

console.log(leo[0]);          // “pingan888”  ⚠️ 差异!

console.log(user[0]);         // “leo”        ⚠️ 差异!

console.log(leo[2][“name”]);  // “pingan999”

console.log(user[2][“name”]); // “pingan999”

3. Array.prototype.concat()

语法:var new_array = old_array.concat(value1[, value2[, ...[, valueN]]])``concat() 方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。详细介绍,可以阅读文档《MDN Array concat》。

let user  = [{name: “leo”},   {age: 18}];

let user1 = [{age: 20},{addr: “fujian”}];

let user2 = user.concat(user1);

user1[0][“age”] = 25;

console.log(user);  // [{“name”:“leo”},{“age”:18}]

console.log(user1); // [{“age”:25},{“addr”:“fujian”}]

console.log(user2); // [{“name”:“leo”},{“age”:18},{“age”:25},{“addr”:“fujian”}]

Array.prototype.concat 也是一个浅拷贝,只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是对象的话只会拷贝一份相同的内存地址。

4. 拓展运算符(…)

语法:var cloneObj = { ...obj };扩展运算符也是浅拷贝,对于值是对象的属性无法完全拷贝成2个不同对象,但是如果属性都是基本类型的值的话,使用扩展运算符也是优势方便的地方。

let user = { name: “leo”, skill: { JavaScript: 90, CSS: 80}};

let leo = {…user};

leo.name = “leo1”;

leo.skill.CSS = 90;

console.log(leo.name);      // “leo1” ⚠️ 差异!

console.log(user.name);     // “leo”  ⚠️ 差异!

console.log(leo.skill.CSS); // 90

console.log(user.skill.CSS);// 90

3.3 手写浅拷贝

实现原理:新的对象复制已有对象中非对象属性的值和对象属性的**「引用」**,也就是说对象属性并不复制到内存。

function cloneShallow(source) {

let target = {};

for (let key in source) {

if (Object.prototype.hasOwnProperty.call(source, key)) {

target[key] = source[key];

}

}

return target;

}

  • 「for in」

for…in语句以任意顺序遍历一个对象自有的、继承的、可枚举的、非Symbol的属性。对于每个不同的属性,语句都会被执行。

  • 「hasOwnProperty」

该函数返回值为布尔值,所有继承了 Object 的对象都会继承到 hasOwnProperty 方法,和 in 运算符不同,该函数会忽略掉那些从原型链上继承到的属性和自身属性。语法:obj.hasOwnProperty(prop)``prop 是要检测的属性**「字符串名称」**或者Symbol

4. 深拷贝


4.1 概念

复制变量值,对于引用数据,则递归至基本类型后,再复制。深拷贝后的对象**「与原来的对象完全隔离」**,互不影响,对一个对象的修改并不会影响另一个对象。

4.2 实现深拷贝

1. JSON.parse(JSON.stringify())

其原理是把一个对象序列化成为一个JSON字符串,将对象的内容转换成字符串的形式再保存在磁盘上,再用JSON.parse() 反序列化将JSON字符串变成一个新的对象。

let user = { name: “leo”, skill: { JavaScript: 90, CSS: 80}};

let leo = JSON.parse(JSON.stringify(user));

leo.name = “leo1”;

leo.skill.CSS = 90;

console.log(leo.name);      // “leo1” ⚠️ 差异!

console.log(user.name);     // “leo”  ⚠️ 差异!

console.log(leo.skill.CSS); // 90 ⚠️ 差异!

console.log(user.skill.CSS);// 80 ⚠️ 差异!

JSON.stringify() 使用注意:

  • 拷贝的对象的值中如果有函数, undefinedsymbol 则经过 JSON.stringify() `序列化后的JSON字符串中这个键值对会消失;

  • 无法拷贝不可枚举的属性,无法拷贝对象的原型链;

  • 拷贝 Date 引用类型会变成字符串;

  • 拷贝 RegExp 引用类型会变成空对象;

  • 对象中含有 NaNInfinity-Infinity ,则序列化的结果会变成 null

  • 无法拷贝对象的循环应用(即 obj[key] = obj )。

2. 第三方库

4.3 手写深拷贝

核心思想是**「递归」**,遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝。实现代码:

const isObject = obj => typeof obj === ‘object’ && obj != null;

function cloneDeep(source) {

if (!isObject(source)) return source; // 非对象返回自身

const target = Array.isArray(source) ? [] : {};

for(var key in source) {

if (Object.prototype.hasOwnProperty.call(source, key)) {

if (isObject(source[key])) {

target[key] = cloneDeep(source[key]); // 注意这里

} else {

target[key] = source[key];

}

}

}

return target;

}

该方法缺陷:遇到循环引用,会陷入一个循环的递归过程,从而导致爆栈。其他写法,可以阅读《如何写出一个惊艳面试官的深拷贝?》 。

5. 小结


「浅拷贝」:将对象的每个属性进行依次复制,但是当对象的属性值是引用类型时,实质复制的是其引用,当引用指向的值改变时也会跟着变化。

「深拷贝」:复制变量值,对于引用数据,则递归至基本类型后,再复制。深拷贝后的对象**「与原来的对象完全隔离」**,互不影响,对一个对象的修改并不会影响另一个对象。

「深拷贝和浅拷贝是针对复杂数据类型来说的,浅拷贝只拷贝一层,而深拷贝是层层拷贝。」

三、垃圾回收机制(GC)

============

垃圾回收(Garbage Collection,缩写为GC)是一种自动的存储器管理机制。当某个程序占用的一部分内存空间不再被这个程序访问时,这个程序会借助垃圾回收算法向操作系统归还这部分内存空间。垃圾回收器可以减轻程序员的负担,也减少程序中的错误。垃圾回收最早起源于LISP语言。目前许多语言如Smalltalk、Java、C#和D语言都支持垃圾回收器,我们熟知的 JavaScript 具有自动垃圾回收机制。

「在 JavaScript 中,原始类型的数据被分配到栈空间中,引用类型的数据会被分配到堆空间中。」

1. 栈空间中的垃圾回收


当函数 showName 调用完成后,通过下移 ESP(Extended Stack Pointer)指针,来销毁 showName 函数,之后调用其他函数时,将覆盖掉旧内存,存放另一个函数的执行上下文,实现垃圾回收。图片来自《浏览器工作原理与实践》

2. 堆空间中的垃圾回收


堆中数据垃圾回收策略的基础是:「代际假说」(The Generational Hypothesis)。即:

  1. 大部分对象在内存中存在时间极短,很多对象很快就不可访问。

  2. 不死的对象将活得更久。

这两个特点不仅仅适用于 JavaScript,同样适用于大多数的动态语言,如 Java、Python 等。V8 引擎将堆空间分为**「新生代」(存放生存「时间短」的对象)和「老生代」(存放生存「时间长」**的对象)两个区域,并使用不同的垃圾回收器。

  • 副垃圾回收器,主要负责新生代的垃圾回收。

  • 主垃圾回收器,主要负责老生代的垃圾回收。

不管是哪种垃圾回收器,都使用相同垃圾回收流程:「标记活动对象和非活动对象,回收非活动对象的内存,最后内存整理。」**

1.1 副垃圾回收器

使用 Scavenge 算法处理,将新生代空间对半分为两个区域,一个对象区域,一个空闲区域。图片来自《浏览器工作原理与实践》

执行流程:

  • 新对象存在在**「对象区域」**,当对象区域将要写满时,执行一次垃圾回收;

  • 垃圾回收过程中,首先对对象区域中的垃圾做标记,然后副垃圾回收器将存活的对象复制并有序排列到空闲区域,相当于完成内存整理。

  • 复制完成后,将对象区域和空闲区域翻转,完成垃圾回收操作,这也让新生代中两块区域无限重复使用。

当然,这也存在一些问题:若复制操作的数据较大则影响清理效率。JavaScript 引擎的解决方式是:将新生代区域设置得比较小,并采用对象晋升策略(经过两次回收仍存活的对象,会被移动到老生区),避免因为新生代区域较小引起存活对象装满整个区域的问题。

1.2 主垃圾回收器

分为:「标记 - 清除(Mark-Sweep)算法」,和**「标记 - 整理(Mark-Compact)算法」**。

「a)标记 - 清除(Mark-Sweep)算法」****「过程:」

  • 标记过程:从一组根元素开始遍历整个元素,能到达的元素为活动对象,反之为垃圾数据;

  • 清除过程:清理被标记的数据,并产生大量碎片内存。(缺点:导致大对象无法分配到足够的连续内存)

图片来自《浏览器工作原理与实践》

「b)标记 - 整理(Mark-Compact)算法」****「过程:」

  • 标记过程:从一组根元素开始遍历整个元素,能到达的元素为活动对象,反之为垃圾数据;

  • 整理过程:将所有存活的对象,向一段移动,然后清除端边界以外的内容。

图片来自《浏览器工作原理与实践》

3. 拓展阅读


1.《图解Java 垃圾回收机制》2.《MDN 内存管理》

四、对象方法和 this

============

1. 对象方法


具体介绍可阅读 《MDN 方法的定义》 。将作为对象属性的方法称为“对象方法”,如下面 user 对象的 say 方法:

let user = {};

let say = function(){console.log(“hello!”)};

user.say = say;  // 赋值到对象上

user.say(); // “hello!”

也可以使用更加简洁的方法:

let user = {

say: function(){}

// 简写为

say (){console.log(“hello!”)}

// ES8 async 方法

async say (){/…/}

}

user.say();

当然对象方法的名称,还支持计算的属性名称作为方法名:

const hello = “Hello”;

let user = {

‘say’ + hello{console.log(“hello!”)}

}

user’say’ + hello; // “hello!”

另外需要注意的是:所有方法定义不是构造函数,如果您尝试实例化它们,将抛出TypeError

let user = {

say(){};

}

new user.say; // TypeError: user.say is not a constructor

2. this


2.1 this 简介

当对象方法需要使用对象中的属性,可以使用 this 关键字:

let user = {

name : ‘leo’,

say(){ console.log(hello ${this.name})}

}

user.say(); // “hello leo”

当代码 user.say() 执行过程中, this 指的是 user 对象。当然也可以直接使用变量名 user 来引用 say() 方法:

let user = {

name : ‘leo’,

say(){ console.log(hello ${user.name})}

}

user.say(); // “hello leo”

但是这样并不安全,因为 user 对象可能赋值给另外一个变量,并且将其他值赋值给 user 对象,就可能导致报错:

let user = {

name : ‘leo’,

say(){ console.log(hello ${user.name})}

}

let leo = user;

user = null;

leo.say(); // Uncaught TypeError: Cannot read property ‘name’ of null

但将  user.name  改成 this.name 代码便正常运行。

2.2 this 取值

this 的值是在 「代码运行时计算出来」 的,它的值取决于代码上下文:

let user = { name: “leo”};

let admin = {name: “pingan”};

let say = function (){

console.log(hello ${this.name})

};

user.fun = say;

admin.fun = say;

// 函数内部 this 是指“点符号前面”的对象

user.fun();     // “hello leo”

admin.fun();    // “hello pingan”

admin’fun’; // “hello pingan”

规则:如果 obj.fun() 被调用,则 thisfun 函数调用期间是 obj ,所以上面的 this 先是 user ,然后是 admin

但是在全局环境中,无论是否开启严格模式, this 都指向全局对象

console.log(this == window); // true

let a = 10;

this.b = 10;

a === this.b; // true

2.3 箭头函数没有自己的 this

箭头函数比较特别,没有自己的 this ,如果有引用 this 的话,则指向外部正常函数,下面例子中, this 指向 user.say() 方法:

let user = {

name : ‘leo’,

say : () => {

console.log(hello ${this.name});

},

hello(){

let fun = () => console.log(hello ${this.name});

fun();

}

}

user.say();   // hello      => say() 外部函数是 window

user.hello(); // hello leo  => fun() 外部函数是 hello

2.4 call / apply / bind

详细可以阅读《js基础-关于call,apply,bind的一切》 。当我们想把 this 值绑定到另一个环境中,就可以使用 call / apply / bind 方法实现:

var user = { name: ‘leo’ };

var name = ‘pingan’;

function fun(){

return console.log(this.name); // this 的值取决于函数调用方式

}

fun();           // “pingan”

fun.call(user);  // “leo”

fun.apply(user); // “leo”

注意:这里的 var name = 'pingan'; 需要使用 var 来声明,使用 let 的话, window 上将没有 name 变量。

三者语法如下:

fun.call(thisArg, param1, param2, …)

fun.apply(thisArg, [param1,param2,…])

fun.bind(thisArg, param1, param2, …)

五、构造函数和 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,

};

for (let key in user) console.log(key); // name, age (no symbols)

// 使用 Symbol 任务直接访问

console.log( "Direct: " + userid );

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);

}

}

userleo; // ‘leo’

// 上面等价于 更简洁

let user = {

leo{

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()

总结一下

面试前要精心做好准备,简历上写的知识点和原理都需要准备好,项目上多想想难点和亮点,这是面试时能和别人不一样的地方。

还有就是表现出自己的谦虚好学,以及对于未来持续进阶的规划,企业招人更偏爱稳定的人。

开源分享:【大厂前端面试题解析+核心总结学习笔记+真实项目实战+最新讲解视频】

万事开头难,但是程序员这一条路坚持几年后发展空间还是非常大的,一切重在坚持。

为了帮助大家更好更高效的准备面试,特别整理了《前端工程师面试手册》电子稿文件。

前端面试题汇总

JavaScript

性能

linux

前端资料汇总

前端工程师岗位缺口一直很大,符合岗位要求的人越来越少,所以学习前端的小伙伴要注意了,一定要把技能学到扎实,做有含金量的项目,这样在找工作的时候无论遇到什么情况,问题都不会大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值