关于javascript:11w字-|-初中级前端-JavaScript-自测清单-2

45次阅读

共计 25137 个字符,预计需要花费 63 分钟才能阅读完成。

前言

在《初中级前端 JavaScript 自测清单 – 1》局部中,和大家简略过了一遍 JavaScript 基础知识,没看过的敌人能够回顾一下????

本系列文章是我在咱们团队外部的“古代 JavaScript 突击队 ”,第一期学习内容为《古代 JavaScript 教程》系列的 第二局部 输入内容,心愿这份自测清单,可能帮忙大家坚固常识,温故知新。

本局部内容,以 JavaScript 对象 为主,大抵包含以下内容:

一、对象

JavaScript 有八种数据额类型,有七种原始类型,它们值只蕴含一种类型(字符串,数字或其余),而对象是用来 保留键值对和更简单实体。
咱们能够通过应用带有可选 属性列表 的花括号 **{...}** 来创建对象,一个属性就是一个键值对 {"key" : "value"},其中键(key)是一个字符串(或称属性名),值(value)能够是任何类型。

1. 创建对象

咱们能够应用 2 种形式来创立一个新对象:

// 1. 通过“构造函数”创立
let user = new Object();

// 2. 通过“字面量”创立
let user = {};

2. 对象文本和属性

创建对象时,能够初始化对象的一些属性:

let user = {
    name : 'leo',
  age  : 18
}

而后能够对该对象进行属性对 增删改查 操作:

// 减少属性
user.addr = "China";
// user => {name: "leo", age: 18, addr: "China"}

// 删除属性
delete user.addr
// user => {name: "leo", age: 18}

// 批改属性
user.age  = 20;
// user => {name: "leo", age: 20}

// 查找属性
user.age;
// 20

3. 方括号的应用

当然对象的键(key)也能够是多词属性,但必须加引号,应用的时候,必须应用方括号([])读取:

let user = {
    name : 'leo',
  "my interest" : ["coding", "football", "cycling"]
}
user["my interest"]; // ["coding", "football", "cycling"]
delete user["my interest"];

咱们也能够在方括号中应用变量,来获取属性值:

let key = "name";
let user = {
    name : "leo",
  age  : 18 
}
// ok
user[key]; // "leo"
user[key] = "pingan";

// error
user.key; // undefined

4. 计算属性

创建对象时,能够在对象字面量中应用方括号,即 计算属性

let key = "name";
let inputKey = prompt("请输出 key", "age");
let user = {[key] : "leo",
  [inputKey] : 18
}
// 当用户在 prompt 上输出 "age" 时,user 变成上面样子:// {name: "leo", age: 18}

当然,计算属性也能够是表达式:

let key = "name";
let user = {["my_" + key] : "leo"
}
user["my_" + key]; // "leo"

5. 属性名简写

理论开发中,能够将雷同的属性名和属性值简写成更短的语法:

// 本来书写形式
let getUser = function(name, age){
  // ...
    return {
        name: name,
    age: age
    }
}

// 简写形式
let getUser = function(name, age){
  // ...
    return {
        name,
    age
    }
}

也能够混用:

// 本来书写形式
let getUser = function(name, age){
  // ...
    return {
        name: name,
    age: 18
    }
}

// 简写形式
let getUser = function(name, age){
  // ...
    return {
        name,
    age: 18
    }
}

6. 对象属性存在性检测

6.1 应用 in 关键字

该办法能够判断 对象的自有属性和继承来的属性 是否存在。

let user = {name: "leo"};
"name" in user;            //true,自有属性存在
"age"  in user;            //false
"toString" in user;     //true,是一个继承属性

6.2 应用对象的 hasOwnProperty() 办法。

该办法只能判断 自有属性 是否存在,对于 继承属性 会返回 false

let user = {name: "leo"};
user.hasOwnProperty("name");       //true,自有属性中有 name
user.hasOwnProperty("age");        //false,自有属性中不存在 age
user.hasOwnProperty("toString");   //false,这是一个继承属性,但不是自有属性

6.3 用 undefined 判断

该办法能够判断对象的 自有属性和继承属性

let user = {name: "leo"};
user.name !== undefined;        // true
user.age  !== undefined;        // false
user.toString !== undefined     // true

该办法存在一个问题,如果属性的值就是 undefined  的话,该办法不能返回想要的后果:

let user = {name: undefined};
user.name !== undefined;        // false,属性存在,但值是 undefined
user.age  !== undefined;        // false
user.toString !== undefined;    // true

6.4 在条件语句中直接判断

let user = {};
if(user.name) user.name = "pingan";
// 如果 name 是 undefine, null, false, " ", 0 或 NaN, 它将放弃不变

user; // {}

7. 对象循环遍历

当咱们须要遍历对象中每一个属性,能够应用 for...in 语句来实现

7.1 for…in 循环

for...in 语句以任意程序遍历一个对象的除 Symbol 以外的可枚举属性。
留神for...in 不应该利用在一个数组,其中索引程序很重要。

let user = {
    name : "leo",
  age  : 18
}

for(let k in user){console.log(k, user[k]);
}
// name leo
// age 18

7.2 ES7 新增办法

ES7 中新减少的 Object.values()Object.entries() 与之前的 Object.keys() 相似,返回数组类型。

1. Object.keys()

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

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,
  [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('pingan');
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 程度并查缺补漏,温故知新。

正文完
 0