# 🌵 JavaScript
# 001: JS 中的数据类型有哪些,区别是什么?
原始类型有(可以用 typeof 检查):Boolean 、String 、Number 、Undefined 、Null 、Symbol(ES6) 、BigInt(ES2020)
对象类型有:对象 Object(包含普通对象 Object 、数组 Array 、函数 Function 、正则 RegExp 、日期 Date 、数学函数 Math )
不同之处:原始类型储存值,对象类型储存地址。
注意点:
(1)闭包变量存储在堆内存;
(2)typeof null -> object,但 null 严谨来说不是对象,是历史遗留的 bug,000 开头代表是对象然而 null 表示为全零,所以将它错误的判断为 object;
(3)number 是浮点类型,浮点运算时会有 bug。
(4)BigInt 是一种新的数据类型,用于当整数值大于 Number 数据类型支持的范围时。这种数据类型允许我们安全地对大整数执行算术操作,表示高分辨率的时间戳,使用大整数 id,等等,而不需要使用库。9007199254740992 === 9007199254740993; // true (Number的最大安全数为2^53-1) 9007199254740992 === 9007199254740993n; // false (要创建BigInt,只需要在数字末尾追加n即可)
# 0.1 + 0.2 === 0.3 打印什么
false。
JavaScirpt 使用 Number 类型来表示数字(包括整数或浮点数),遵循 IEEE 754 标准 通过 64 位来表示一个数字。
第 0 位:符号位 a,0 表示正数 ,1 表示负数;第 1 位到第 11 位:储存指数部分 e;第 12 位到第 63 位:储存小数部分(即有效数字)f。
二进制表示有效数字总是 1.xx…xx 的形式,有效数字尾数部分 f 在规约形式下第一位默认为 1(省略不写,xx..xx 为尾数部分 f,最长 52 位)。因此,JavaScript 提供的有效数字最长为 53 个二进制位(64 位浮点的后 52 位+被省略的 1 位)因此 js 最大安全数是 Number.MAX_SAFE_INTEGER === Math.pow(2,53) - 1
, 而不是 Math.pow(2,52) - 1
在两数相加时,计算机无法直接对十进制的数字进行运算,这是硬件物理特性已经决定的。先按照 IEEE 754 转成相应的二进制,然后对阶运算。
0.1 和 0.2 转换成二进制的时候尾数会发生无限循环,且由于 IEEE 754 尾数位数限制,需要将后面多余的位截掉;
由于指数位数不相同,运算时需要对阶运算(两个进行运算的浮点数的阶码对齐,使两个浮点数的尾数能够对齐进行加减运算),JS 引擎对二进制进行截断,所以造成精度丢失。
(本质是 二进制模拟十进制进行计算时 的精度问题。)
相关文章:0.1 + 0.2 不等于 0.3 (opens new window)
# 为什么 x=0.1 能得到 0.1?
这是因为这个 0.1 并不是真正的 0.1,JS 最大可以表示 2^53(9007199254740992), 长度是 16,所以可以使用 toPrecision(16) 来做精度运算。超过的精度会自动做凑整处理。
0.10000000000000000555.toPrecision(16)
// 返回 0.1000000000000000,去掉末尾的零后正好为 0.1
// 但来一个更高的精度:
0.1.toPrecision(21) = 0.100000000000000005551
# 怎么解决精度问题?
(1)将数字转成整数
function add(num1, num2) {
const num1Base = num1.split(".")[1].length || [].length;
const num2Base = num2.split(".")[1].length || [].length;
const parse = Math.pow(10, Math.max(num1Base, num2Base));
return (num1 * parse + num2 * parse) / parse;
}
(2)三方库 - Math.js
# JS 整数是怎么表示
通过 Number 类型来表示,遵循 IEEE754 标准,通过 64 位来表示一个数字,(1 + 11 + 52),最大安全数字是 Math.pow(2, 53) - 1,对应是 16 位十进制。(符号位 + 指数位 + 小数部分有效位)
Number() 的存储空间是 Math.pow(2, 53) ,53 为有效数字,等于 JS 能支持的最大数字。如果接收到一个超过范围的数字会自动被截断。
# 002: 请写出如下代码的打印结果。
let name = "chieminchan";
function changeTest(name) {
name = "kate";
}
changeTest(name);
console.log(name); // -> chieminchan
// 这里把 name 变量对应的值 chieminchan 传入函数 changeTest,然后复制给了的一个局部变量,改变局部变量的值不会对外部变量产生影响,因此打印的依然的是 chieminchan
const person1 = {
name: "chieminchan",
age: 20,
};
function changeTest(person) {
person.age = 22;
person = {
name: "jack",
age: 24,
};
return person;
}
const person2 = changeTest(person1);
console.log(person1); // -> ?
console.log(person2); // -> ?
这里看似成了地址传递,但其实这一段和上一段 js 代码的函数传参方式都是值传递。
此处将 person1 对应的值传入了 changeTest 函数,同样地将这个值复制了一个副本给函数里面的局部变量,只不过这个局部变量的值是指向堆内存中的地址。这时候,在函数内部对这个局部变量的属性进行修改,就变成了对 这个局部变量 所指向的堆内存地址 的值进行修改。
因为外部变量 person1 的值也是指向同一个堆内存的地址,因此外部变量的值也一并发生了变化。
console.log(person1); // -> { name: 'chieminchan', age : 22 }
console.log(person2); // -> { name: 'jack', age: 24 }
考察点:ECMAScript 所有的函数的参数都是按值传递,即函数参数传递的是变量的值。
# 003: 列出判断数据类型的方法和区别。
# (1)常用的 typeof
对于原始类型来说,除了 null,typeof 可以正确显示对应的数据类型:
typeof 1; // 'number'
typeof "1"; //'string'
typeof True; // 'boolean'
typeof undefined; //'undefined'
typeof Symbol(); // 'symbol'
typeof new String("A"); //"object",new String会返回一个对象
typeof String("A"); //"string",String 会返回一个字符串
但对于对象类型来说,除了函数,都会显示object
,因此typeof
不能准确地对数据类型进行判断:
typeof []; // 'object'
typeof {}; //'object'
typeof function() {}; // 'function'
// 特殊例子
typeof null; //'object'
typeof Object; // 'function'
typeof Object(); // 'object'
# (2)instanceof(内部机制是通过原型链来判断,一般用来判断对象类型)
const Person = function() {};
const p1 = new Person();
p1 instanceof Person; // true
const str1 = new String("hello world");
str1 instanceof String; // true
const a = ["1", "2"];
a instanceof Array; //true;
a instanceof Object; //true;
null instanceof object; //false;
# (3) Object.prototype.toString.call
Object.prototype.toString.call(function() {}); // "[object Function]";
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call("1"); // "[object String]"
Object.prototype.toString.call(new String(1)); // "[object String]"
Object.prototype.toString.call(2); //"[object Number]"
Object.prototype.toString.call(null); //"[object Null]"
参考文章:
浅谈 instanceof 和 typeof 的实现原理 (opens new window)
# typeof 实现原理
typeof 是通过获取数据底层的类型标签来获取类型信息。
JS 在底层存储变量的时候,会在变量的机器码的低位 1-3 位存储其类型信息:
000:对象
010:浮点数
100:字符串
110:布尔
1:整数
对于 undefined
和 null
来说,这两个值的信息存储是有点特殊,nul
l:所有机器码均为 0,undefined
:用 −2^30 整数来表示。
typeof 在判断 null 的时候就出现问题了,由于 null 的所有机器码均为 0,因此直接被当做了对象来看待。
# instanceof 实现原理
instanceof 的实现原理:
instanceof 主要的实现原理就是只要右边变量的 prototype 在左边变量的原型链上即可。
因此,instanceof 在查找的过程中会遍历左边变量的原型链,直到找到右边变量的 prototype,如果查找失败,则会返回 false,告诉我们左边变量并非是右边变量的实例。
# instanceof 手写实现
function _instanceof(instance, right) {
const rightProto = right.prototype;
const instanceProto = instance.__proto__;
while (true) {
if (instanceProto === rightProto) {
return true;
}
if (instanceProto === null) {
return false;
}
instanceProto = instanceProto.__proto_;
}
}
# 顺带引出一些例子
// Function Object都是由函数构造
Function instanceof Function; // true
Function instanceof Object; // true
//Object.__proto__ = Object.prototype
// Object.prototype为一个对象,
Object instanceof Object; // true
Object instanceof Funciton; // true(对象由函数创建)
function Foo() {}
Foo instanceof Foo; // false
Foo instanceof Function; // true
Foo instanceof Object; // true
# 但对于原始类型不能直接通过 instanceof 来判断类型,特殊例子:
var str = "hello world";
str instanceof String; // false
let str2 = new String("abc");
typeof str2; // "object"
str2 instanceof String; // true
需要利用Symbol.hasInstance (opens new window)将原有的 instanceof 方法重定义,换成了 typeof,才能能够判断基本数据类型:
class PrimitiveNumber {
static [Symbol.hasInstance](x) {
return typeof x === "number";
}
}
console.log(111 instanceof PrimitiveNumber); // true
# 004: == 和 ===有什么区别?
=== 为严格相等,而 == 比 === 条件更宽松。
=== 要求两边的值和类型都要相同, == 在比较时只要求值相同。
== 比较值的规则:
(1)首先会判断两者类型是否相同。相同的话就是比大小了;类型不相同的话,那么就会进行类型转换:
(2)会先判断是否在对比 null 和 undefined,是的话返回 true ;
null == undefined -> true
(3)判断两者类型是否为 string 和 number,是的话就会将字符串 转换为 Number;
1 == '1' -> 1 == 1 -> true
(4)判断其中一方是否是 Boolean,是的话就把 Boolean 转换成 Number,再进行比较;
'1' == true -> '1' == 1 -> 1 == 1 -> true
PS: 请记住 6 个虚值(null,undefined,'',0,NaN,false) -> 转成布尔都会是 false
(5)如果其中一方为 Object,且另一方为 String、Number 或者 Symbol,会将 Object 转换成 原始类型,再进行比较。
(PS:这里会优先通过 valueOf 进行转换,如果 valueOf 输出的值类型是引用类型,将会通过 toString 进行转换)
{ a: 1 } == 1
-> '[object Object]' == 1
-> NaN == 1
-> false
[] == ![]
-> [] == false (!的优先级是大于 == 的,先执行后者)
-> [] == 0 (按照第(4)点来,其中一方是 Boolean 要先转换成 Number)
-> '' == 0 (按照第(5)点来,一方为Object,需valueOf转成原始类型)
-> 0 == 0 (按照第(3)点来,''转数字,Number('')值为0)
-> true
//如果两个操作数都是对象,则比较它们是不是同一个对象引用,如果两个操作数都指向同一个对象
[] == {} -> false
// 验证(5)
const test = { a: 1 };
test.valueOf = () => {
console.log('valueOf');
return this;
}
test.toString = () => {
console.log('toString');
return '1'
}
console.log(test == 1) // valueOf toString true
(更详细规则详情戳这里 (opens new window))
# 加号的隐形转换
在加号中,会使用 ToPrimitive 运算转换左与右运算元为原始数据类型值(primitive)
在第 1 步转换后,如果有运算元出现原始数据类型是"字符串"类型值时,则另一运算元作强制转换为字符串,然后作字符串的连接运算(concatenation)
在其他情况时,所有运算元都会转换为原始数据类型的"数字"类型值,然后作数学的相加运算(addition)
[]+{} = ?
{}+[] = ?
引用知乎的回答:https://www.zhihu.com/question/45478070?sort=created:
先说
[] + {}
。
一个数组加一个对象。加法会进行隐式类型转换,规则是调用其 valueOf() 或 toString() 以取得一个非对象的值(primitive value)。
如果两个值中的任何一个是字符串,则进行字符串串接,否则进行数字加法。
[] 和 {} 的 valueOf() 都返回对象自身(一个空数组和一个空对象),所以都会调用 toString(),最后的结果是字符串串接。
[].toString() 返回空字符串,({}).toString() 返回“[object Object]”。最后的结果就是“[object Object]”。
{} + []
{} 除了表示一个对象之外,也可以表示一个空的 block。在
{} + []
中,如果{}(空对象)
在前面,而[](空数组)
在后面时,前面(左边)那个运算元会被认为是区块语句而不是对象字面量。所以
{} + []
相当于+[]
语句,也就是相当于强制求出数字值的 Number([])运算,相当于 Number("")运算,最后得出的是 0 数字。{} 被解析为空的 block,随后的 + 被解析为正号运算符。即实际上被解析成成了:
{ // empty block }
。+[]
即对一个空数组执行正号运算,实际上就是把数组转型为数字。首先调用 [].valueOf(),返回数组自身,不是 primitive value,因此继续调用 [].toString() ,返回空字符串。空字符串转型为数字,返回 0,即最后的结果。
# 005: 如何理解作用域链,作用域链的尽头是什么?(针对变量)
(1)作用域是定义变量的区域,可确定当前执行代码对变量的访问权限,同时也决定了变量的生命周期。Javascript 采用的是词法作用域(就是静态作用域),函数的作用域基于函数创建的位置,函数一旦创建了就会产生一个作用域,函数内部会有一个 [scope]
属性;
(2)作用域链可以理解为在函数执行时,用于搜索变量的一条链子。当在一个函数中访问某个变量时,会先在当前函数上下文的变量对象中作用中检索,如果没检索到这个变量,逐步往上一级的变量对象检索,一直到全局上下文的变量对象(浏览器环境下全局对象 window)。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
var value = 1;
function bar() {
var value = 2;
function foo() {
console.log(value);
}
return foo();
}
bar(); // 2
执行函数 foo,因为 foo 的创建位置在 bar 函数体内,因此在 foo 自身作用域搜寻不到变量 value 时,会往上一层搜寻,即 bar 函数体内,找到 value 为 2,因此结果输出 2。
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar(); // 1
因为是静态作用域,执行 foo 函数时候会根据创建的位置,先从自身的局部作用域搜寻变量 value,找不到就往上搜寻,于是 value 为 1 ,结果也会输出 1。
再来个例子,猜猜输出什么:
function bar() {
var myName = " 极客世界 ";
let test1 = 100;
if (1) {
debugger;
let myName = "Chrome 浏览器 ";
console.log("a", test);
}
}
function foo() {
var myName = " 极客邦 ";
let test = 2;
{
let test = 3;
bar();
}
}
var myName = " 极客时间 ";
let myAge = 10;
let test = 1;
foo();
# 006: 什么是闭包,为什么要用它?
# 对闭包的理解
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量。当通过调用一个外部函数 A 返回一个内部函数 B 后,即使该外部函数 A 已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。
如:
function f1() {
var a = 2;
function f2() {
console.log(a); //2
}
return f2;
}
var x = f1();
x(); // 2
# 闭包产生的原因
由于作用域链从最底层向上找,从当前作用域找到全局作用域 window 为止。如果全局还没有的话就会报错。因此,为了在当前环境中能存在指向局部作用域的引用,可以间接访问函数内部的变量,闭包就出现了。
# 常见的闭包使用方式
(1) 中作为函数返回值被返回;
作为函数参数传递;
var a = 1; function test1() { var a = 2; // 闭包 function test2() { console.log(a); } test3(test2); } function test3(fn) { // 闭包 fn(); } test1(); // 2
在定时器、事件监听、Ajax 请求或者任何异步中,只要使用了回调函数,实际上就是在使用闭包;
// 事件监听 $("#app").click(function() { console.log("DOM Listener"); }); // 定时器 setTimeout(function() { console.log("in settiomeout"); }, 0);
IIFE(立即执行函数表达式)创建闭包。
for (var i = 1; i <= 5; i++) { setTimeout(function timer() { console.log(i); }, 0); }
按照 js 事件循环的机制,setTimeout 是宏任务,等待主线程的循环任务执行完毕,接着会执行宏任务 setTimeout 的回调,因为在回调函数的局部作用域中没有变量 i,因此会依次往上找到全局变量 i,因为此时循环已结束,所以 i 已变成了 6,所以会全部输出 6。
可以利用 IIFE 来解决这个问题,当每次 for 循环时,把此时的 i 值作为参数传递到定时器中:
for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, 0); })(i); }
(另外,这个问题的推荐用 es6 的 let 来解决。)
# 闭包的回收机制
如果引用闭包的函数是一个全局变量,那么闭包会一直存在直到页面关闭;但如果这个闭包以后不再使用的话,就会造成内存泄漏。
如果引用闭包的函数是个局部变量,等函数销毁后,在下次 JavaScript 引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么 JavaScript 引擎的垃圾回收器就会回收这块内存。
所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。
# 007: 原型和构造函数的关系,如何理解原型链?(针对对象属性)
(1)原型
每个函数数据类型内部都会默认有一个 prototype
属性,这个属性指向函数的原型对象。
而这个原型对象就是这个函数作为构造函数时,通过 new 创建的实例对象的原型。(实例对象通过隐式的 __proto__
指向构造函数的原型对象)
可以说在 new 的过程中,新对象就会被添加了__proto__
并且链接到构造函数的原型对象上。)
(2)原型和构造函数的关系,constructor
实例对象和构造函数可分别通过__proto__
、prototype
指向原型,而原型可通过constructor
属性指向构造函数。(让实例对象知道是什么函数构造了它)
如上图所示:
1)构造函数 Person 的实例对象为 person,Person 通过 prototype 指向自己的原型对象 Person.prototype
;
2)原型对象 Person.prototype
可通过constructor
指向构造函数 Person;
3)实例 person 通过隐式的 __proto__
指向原型 Person.prototype;
4)Person.prototype 原型对象同时也是一个对象,即作为 Object 的实例,因此也有Person.prototype.__ proto__
属性指向自己的原型 Object.prototype。
(3)原型链
原型链就是多个对象通过__proto__
连接起来的一个链子。
在实例对象里面,如果要访问某个属性,首先会从这个实例对象内部搜寻;如果在内部找不到这个属性,就会去它的原型里面找这个属性,若还是没有,就沿着原型链,向它的原型的原型,直到找到为止。
原型链最顶层是 Object 的原型对象 Object.prototype ,而它的原型是 null,因此到此便会终止搜寻。
⚠️ :
a. 对象的 hasOwnProperty() 可用于检查对象自身中是否含有该属性;
b. 使用 in 也可检查对象中是否含有某个属性,如果对象中没有,但是原型链中有,也会返回 true。
相关题:
function Ofo() {}
function Bick() {
this.name = "bick";
}
Ofo.prototype = new Bick();
var OfoBick = new Ofo();
var bick = new Bick();
console.log(OfoBick.name);
console.log(bick.name);
// bick
// bick
# Object.create 的实现
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.sayHi = function() {
console.log(`hi,i am ${this.name}`);
};
var b = Object.create(Person.prototype, {
name: {
value: "coco",
writable: true,
configurable: true,
enumerable: true,
},
sex: {
enumerable: true,
get: function() {
return "hello sex";
},
set: function(val) {
console.log("set value:" + val);
},
},
});
// 以上其实等价于
// var c = new Person("coco", "boy");
Object.create(proto,[propertiesObject])
proto:新创建对象的原型对象
propertiesObject:可选。要添加到新对象的可枚举的属性,包括属性名字属性值以及描述符(新添加的属性是其自身的属性,而不是其原型链上的属性,可用 hasOwnProperty()获取)
Object.create(null) 和 {} 的区别:前者会创建一个纯净不继承 Object 原型链上方法的全空白对象,{}创建的对象会继承 Object 原型链上方法
由以上可知,Object.create 做的事情主要有:
1)创建一个新的对象,并以第一个参数作为新对象的proto属性的值(以第一个参数作为新对象的构造函数的原型对象)
2)有第二个可选参数,是一个对象,对象的每个属性都会作为新对象的自身属性,对象的属性值以 descriptor(Object.getOwnPropertyDescriptor(obj, 'key'))的形式出现,且 enumerable 默认为 false
// step1: 暂时不考虑第二个参数
Object.prototype._create1 = function(proto) {
//实现一个隐藏函数
function F() {}
//函数的原型设置为参数传进来的原型
F.prototype = proto;
// F.prototype.constructor = F;
// 返回一个F函数的实例,即此实例的__proto__指向为参数proto
const newObj = new F();
// 传进来的原型为null时,建立一个全空对象
if (proto === null) {
newObj.__proto__ = null;
}
return newObj;
};
// step2,考虑第二个参数
Object.prototype._create2 = function(proto, properties = null) {
function F() {}
F.prototype = proto;
const newObj = new F();
if (properties) {
// Object.defineProperty定义新属性或修改原有的属性
// Object.defineProperty(obj, prop, descriptor)
// Object.defineProperty(obj, "test", {
// value: 任意类型的值,
// configurable: true | false, // 是否可删除/再次设置特性
// enumerable: true | false, //是否可枚举(使用for...in或Object.keys())
// writable: true | false, //是否可被重写
// });
Object.defineProperties(newObj, properties);
}
if (proto === null) {
newObj.__proto__ = null;
}
return newObj;
};
# 008: new 具体干了些什么?通过 new 的方式创建对象和通过字面量创建有什么区别?模拟实现 new?
(1)new 操作背后的步骤:
- 创建一个空对象,并把这个空对象的原型指向构造函数原型对象;(继承自构造函数原型对象的实例)
- 使用 apply 将构造函数的 this 指向空对象,并执行一遍构造函数,为空对象增加新的属性方法;
- 构造函数有 return 语句,但没有指定返回值,或返回一个原始值,那么这时将忽略返回值,返回这个新对象;若返回的是一个对象,则返回构造函数提供这一个对象。
- 构造函数无 return,则直接返回这个新对象。
(2) 通过 new 创建对象和字面量创建对象的方式区别:
- 字面量创建对象不会调用 Object() 构造函数,简洁明了,性能较好;
- 通过 new Object() 方式创建对象本质上是方法调用,涉及到在原型链中遍历该方法,当找到该方法后,又会生产方法调用必须的堆栈信息,方法调用结束后,还要释放该堆栈,性能会不如字面量的方式。
(3)模拟实现 new:
这里主要参考了大神冴羽 (opens new window)的文章:
function _new() {
// 简单创建一个空对象
var obj = {};
// 获取从外部传入的构造器,即第一个参数
var constructor = [].shift.call(arguments);
// 实例指向的原型
obj.__proto__ = constructor.prototype;
// 也可通过 Object.create 直接创建继承自构造函数原型对象的实例对象
// var obj = Object.create(constructor.prototype);
// 通过改变外部传入的构造器中的this指向,并执行构造器,给obj设置新的属性
var result = constructor.apply(obj, arguments);
// 根据构造器的返回值,来决定该返回什么
// 构造函数有返回,且返回的是一个对象则按构造函数的对象来返回
// 构造函数无返回,即返回 undefined,则返回这个新创建的对象
return typeof result === "object" ? result || obj : obj;
}
# Object.create 、{}、new 的区别?
直接字面量创建 {}
创建一个新对象,并且继承 Object 原型的属性方法。new 关键字创建
创建一个给定的构造函数原型的新实例对象,实例对象会继承构造函数的属性方法。Object.create()
创建一个继承给定的第一个参数原型的新对象,第一个参数为 null 时会返回一个真正的空对象。
# 009: call、apply 和 bind 的区别,模拟实现 bind、call?
# 010: 对 this 指向的理解
(1)call/apply/bind 显示绑定
(2)全局上下文中,this 默认绑定到 window,严格模式下指向 undefined
(3)谁最后调用就指向谁
let foo = {
bar: function() {
console.log(this);
},
};
const func = foo.bar;
func(); // window
👆 func()
是方法调用,this 指向调用者,即全局上下文的情况。
foo.bar(); // foo
👆 foo.bar()
以对象.方法的形式调用,this 指向调用者,即 foo
。
⚠️ 值得注意,有个特殊的情况:
let foo = {
name: "foo",
bar: {
name: "bar",
baz: function() {
console.log(this.name);
},
},
};
let name = "window";
foo.bar.baz(); // bar
若一串对象属性链中的某个属性对应的一个函数中有 this,尽管这个函数是被最外层的对象所调用,this 指向也只会是最内层的对象。
(5)new 绑定
如果函数或者方法调用之前带有关键字 new,就构成构造函数调用。此时的 this 指向构造函数的实例对象。
(6)箭头函数
- 箭头函数中没有 this 绑定,必须通过查找作用域链来决定 this 的值。
如果箭头函数被非箭头函数包含,则 this 指向当前外层最近的非箭头函数的 this,否则,找不到就是 window(严格模式是 undefined)。
const obj1 = {
foo: () => {
console.log(this);
},
};
obj1.foo(); // window
const obj2 = {
foo: function() {
console.log(this);
const bar = () => {
console.log(this);
};
bar();
},
};
obj2.foo();
// obj2 {foo: ƒ}
// obj2 {foo: ƒ}
// bar() 找到最近的非箭头函数为 foo,而此时 foo 是被 obj2 调用,即 foo 中的 this 指向 obj2,因此箭头函数中 this 指向 obj2
const obj3 = {
foo: function() {
console.log(this);
const bar = () => {
console.log(this);
};
bar();
},
};
const baz = obj3.foo;
baz();
// window
// window
// bar() 找到最近的非箭头函数为 foo,而 baz() 是方法调用, 即 baz 中的 this 指向全局上下文,因此箭头函数中 this 指向全局上下文 window
(7)DOM 事件绑定
# 011: 事件流、事件对象、事件处理程序、事件委托
# 012: 深拷贝和浅拷贝,哪些栗子?
当目标对象对赋值对象通过直接赋值的方式进行拷贝,这种情况仅能拷贝了地址,改变一方属性值,由于是同一个引用,会导致同个地址对应的属性值都发生变化。
let a = {
age: 1,
};
let b = a;
a.age = 2;
console.log(b.age); // 2
(1)浅拷贝
浅拷贝会将对象的各个属性进行依次复制,但并不会进行递归复制,即只会赋值目标对象的第一层属性。
对于目标对象第一层为基本数据类型的数据,就是直接赋值;
而对于目标对象第一层为引用数据类型的数据,就是直接赋存于栈内存中的堆内存地址。
- Object.assign
Object.assign 会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的依然是属性的引用,因此只是浅拷贝,并不是深拷贝。
const person1 = {
name: "Kate",
age: 20,
habits: {
first: "running",
second: "eating",
},
};
const person2 = Object.assign({}, person1);
person2.name = "Rose";
person2.habits.first = "shopping";
console.log("person1:", person1);
console.log("person2:", person2);
// person1: {name: "Kate", age: 20, habits: {first: "shopping", second: "eating"}}
// person2: {name: "Rose", age: 20, habits: {first: "shopping", second: "eating"}}
- 运算符 ...
const person1 = {
name: "Kate",
age: 20,
habit: {
first: "running",
},
};
const person2 = { ...person1 };
person2.name = "Rose";
person2.habit.first = "reading";
console.log("person1:", person1);
console.log("person2:", person2);
// person1: {name: "Kate", age: 20, habits: {first: "reading"}}
// person1: {name: "Rose", age: 20, habits: {first: "reading"}}
- array.concat()
const arr1 = [20, { habit: "eating" }];
const arr2 = arr1.concat();
arr2[0] = 22;
arr2[1].habit = "running";
console.log("arr1:", arr1);
console.log("arr2:", arr2);
// arr1:[20, {habit: "running"}]
// arr2:[22, {habit: "running"}]
(2)深拷贝
深拷贝不同于浅拷贝,它不只拷贝目标对象的第一层属性,而是递归拷贝目标对象的所有属性。
- JSON.parse(JSON.stringify())
const person1 = {
name: "Kate",
age: 20,
habits: {
first: "running",
},
};
const person2 = JSON.parse(JSON.stringify(person1));
person2.name = "Rose";
person2.habits.first = "eating";
console.log("person1:", person1);
console.log("person2:", person2);
// person1: {name: "Kate", age: 20, habits: {first: "running"}}
// person2: {name: "Rose", age: 20, habits: {first: "eating"}}
这个方法可以足够覆盖大多数的应用场景,但有一些情况会出现问题,比如:
- 在遇到函数、 undefined 或者 symbol 的时候,JSON 不能正常的序列化
const person1 = {
name: "Kate",
age: 20,
sex: Symbol("female"),
habits: undefined,
jobs: function() {},
};
const person2 = JSON.parse(JSON.stringify(person1));
console.log("person1:", person1);
console.log("person2:", person2);
// person1: {name: "Kate", age: 20, sex: Symbol(female), habits: undefined, jobs: ƒ ()}
// person2: {name: "Kate", age: 20}
- 不能解决循环引用的对象
// 拷贝person1会出现系统栈溢出,因为出现了无限递归的情况。
const person1 = {
name: "Kate",
age: 20,
habits: {
first: "eating",
},
};
person1.friend = person1;
const person2 = JSON.parse(JSON.stringify(person1));
console.log("person1:", person1);
console.log("person2:", person2);
// Uncaught TypeError: Converting circular structure to JSON at JSON.stringify (<anonymous>)
# 013: 什么是防抖和节流?有什么区别?如何实现?
防抖和节流的主要作用都是为了减少函数无用的触发次数,以便解决响应跟不上触发频率导致页面卡顿这类问题,提高性能和避免资源浪费。
但防抖动和节流本质是不一样的,防抖是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。
# 防抖 debounce 原理:
在触发触发某个事件后的 n 秒后才能重新执行,若在触发事件的 n 秒内再次触发这个事件,时间 n 将会重新开始计时,直到触发完事件的 n 秒内不再触发事件,这个事件才会执行。(指定时间内只能触发一次事件,否则多次触发会重新计时。)
应用场景:
常用于搜索框输入实时查询、下载按钮
# 节流 throttle 原理:
当持续触发事件时,保证一定时间段内只能执行一次事件处理函数。(指定时间内多次触发时间后,需要间隔一定时间才会执行)
应用场景:
常用于监听浏览器滚动条
# 防抖和节流的手写实现
# 014: 浏览器重绘和回流?
# 015: createDocumentFragment 创建文档碎片节点
<ul id="lists">
<li class="list">123</li>
<li class="list">234</li>
<li class="list">345</li>
<li class="list">456</li>
<li class="list">678</li>
<li class="list">789</li>
</ul>
<div id="text"></div>
<script>
const lists = document.getElementById("lists");
const text = document.getElementById("text");
// 事件委托
lists.addEventListener("click", function(e) {
const target = e.target;
text.innerText = target.textContent;
});
// document.createDocumentFragment
const fragement = document.createDocumentFragment();
for (let i = 0; i < 20; i++) {
let li = document.createElement("li");
li.innerText = i + ". i am a list";
fragement.appendChild(li);
}
lists.append(fragement);
</script>
# 016: for..in 和 for..of 区别
# for (let key in Obj)
对于对象,遍历可枚举的对象属性,包括 prototype 中的方法名称,但不包括 不可枚举 和 Symbol 类型 的属性名称
对于数组,枚举索引(index)
for..in
对于数组存在一个问题:
const arr = [1, 2, 3, 4];
arr.name = "ccc";
for (let index in arr) {
console.log(index); // 0, 1, 2, 3, name
}
console.log(arr.length); // 4
# for (let item of Array)
for..of
为了解决 for..in
的问题,只遍历集合本身的元素,可用于 set、map、array 可迭代对象。
const arr = [1, 2, 3, 4];
arr.name = "ccc";
for (let item of arr) {
console.log(index); // 1, 2, 3, 4
}
# 017:JavaScript 如何实现继承?(不同方式的优缺点)
# 019:JavaScript 的类和 ES6 的 class 区别
# 020:JavaScript 的设计模式
# 021:reduce
https://www.jianshu.com/p/e375ba1cfc47
reduce polyfill
// 简单版
function _reduce(arr = [], fn, initialVal = undefined) {
// 初始化值
let startIndex = 1;
let startVal = arr[0];
if (initialVal !== undefined) {
startIndex = 0;
startVal = initialVal;
}
let result = startVal;
for (let i = startIndex; i < arr.length; i++) {
// (prev, cur, curIndex, arr) => {}
result = fn(result, arr[i], i, arr);
}
return result;
}
_reduce([1, 2, 3, 4], (prev, cur) => {
return prev + cur;
});
// 复杂版
Array.prototype.myreduce = function(fn, initialVal = undefined) {
const sourceArray = this;
const length = sourceArray.length;
// 检验数组长度
if (!length) {
throw new TypeError("empty array");
}
// 检验fn是否为函数
if (typeof fn !== "function") {
throw new TypeError(`${fn} is not a function`);
}
// 初始化默认值
let startIndex = 1;
let startValue = sourceArray[0];
if (initialVal !== undefined) {
startIndex = 0;
startValue = initialVal;
}
let result = startValue;
for (let i = startIndex; i < length; i++) {
result = fn(result, sourceArray[i], i, sourceArray);
}
return result;
};
[1, 2, 3, 4].myreduce((prev, cur) => {
return prev + cur;
}, 2);
# 022:compose
# 023:数组方法中的 indexOf 和 includes 实现
Array.prototype.indexOf = function(param, start = 0) {
//如果start大于等于数组长度,此时this[start]越界,返回-1
if (start >= this.length) {
return -1;
}
//如果start小于0,判断start + this.length
if (start < 0) {
start = start + this.length < 0 ? 0 : this.length + start;
}
//start处理完毕,开始从start处遍历数组,查找元素下标
for(let i = start, i < this.length; i++){
if(this[i] === param) {
return i;
}
}
// 遍历完也没找到,返回-1
return -1;
};
Array.prototype.includes = function (param, start = 0) {
if (start >= this.length) return false;
if (start < 0) {
start = start + this.length < 0 ? 0 : start + this.length;
}
//start处理完毕后,判断要查找的元素是不是NaN,
if (Number.isNaN(param)) {
//param是NaN,从start处开始逐个判断数组中的元素是不是NaN,如果是,返回true
for (let i = start; i < this.length; i++) {
if (Number.isNaN(this[i])) return true;
}
} else {
//param不是NaN,使用===比较即可
for (let i = start; i < this.length; i++) {
if (this[i] === param) return true;
}
}
return false
}
# 024:什么是 Symbol,它的使用场景是什么
新的原始数据类型 Symbol 表示独一无二的值。最大的用法是用来定义对象的唯一属性名,Symbol 作为对象属性名时不能用.运算符,要用方括号obj[symbol]
。
Symbol.for() 可以在全局访问 Symbol。Symbol.for() 类似单例模式,首先会在全局搜索被登记的 Symbol 中是否有该字符串参数作为名称的 Symbol 值,如果有即返回该 Symbol 值,若没有则新建并返回一个以该字符串参数为名称的 Symbol 值,并登记在全局环境中供搜索。
使用场景:
1)Symbol 用来作为对象的属性名,表示唯一的属性名称,防止变量名冲突或;
2) Symbol 不会被常规的方法(除了 Object.getOwnPropertySymbols,Reflect.ownKeys() 外)遍历到,所以可以用来模拟私有变量;更优雅的“对内操作”和“对外选择性输出”;
3)使用 Symbol 来替代常量,防止常量值相等;
const TYPE_AUDIO = Symbol();
const TYPE_VIDEO = Symbol();
const TYPE_IMAGE = Symbol();
function handleFileResource(resource) {
switch (resource) {
case TYPE_AUDIO:
console.log("TYPE_AUDIO");
break;
case TYPE_VIDEO:
console.log("TYPE_VIDEO");
break;
case TYPE_IMAGE:
console.log("TYPE_IMAGE");
break;
default:
throw new Error("Unknown type of resource");
}
}
// 当你想要判断的时候只需要把值传进去
handleFileResource(TYPE_VIDEO); // TYPE_AUDIO
4)注册和获取全局 Symbol
let gs1 = Symbol.for("global_symbol_1"); //注册一个全局Symbol
let gs2 = Symbol.for("global_symbol_1"); //获取全局Symbol
gs1 === gs2; // true
5)注册对象迭代器
Symbol.iterator 可以为每一个对象定义默认的迭代器。该迭代器可以被for…of
和…
运算符循环使用。
插入一下关于 iterator 的知识:
一个数据结构只要具有 iterator 接口,就可以用 for...of 循环遍历它的成员。Iterator 为各种不同的数据结构提供统一的访问机制。Iterator 其实就是一个具有 next()方法的对象,并且每次调用 next()方法都会返回一个结果对象,这个结果对象有 value 和 done 两个属性。
Symbol.iterator 会返回一个对象,是一个遍历器对象,而作为遍历器对象,其必须具备的特征就是必须具备 next()方法。
for 循环会不断调用 next 。调用 next 函数,返回 value 和 done 两个属性;value 属性返回当前位置的成员,done 属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用 next 方法;当 done 为 true 时,即遍历完成。
var obj = {
k1: "a",
k2: "b",
k3: "c",
};
Object.defineProperty(obj, Symbol.iterator, {
// 默认情况下通过defineProperty定义的属性是不能被枚举(遍历)的
enumberable: false, // 是否可枚举
configurable: false, // 是否可以配置对象,删除属性
writable: false, // 是否可以修改对象
value: function() {
var _this = this;
var index = 0;
var keys = Object.keys(_this);
return {
next: function() {
const done = index < keys.length ? false : true;
const value =
index < keys.length ? _this[keys[index++]] : undefined;
return { value, done };
},
};
},
});
for (let item of obj) {
console.log(item); // a b c
}
留一个问题:有没有其他迭代生成器的写法?
https://cloud.tencent.com/developer/article/1446889
# 025:Object.keys、Object.getOwnPropertyNames、Object.getPropertySymbols
Object.keys:会返回一个给定对象的自身可枚举属性组成的数组,会遍历原型上的属性,数组中属性名的排列顺序和使用for...in
循环遍历该对象时返回的顺序一致。
Object.getOwnPropertyNames:返回一个给定对象的所有可枚举或不可枚举的属性名(但不包括 Symbol 值作为名称的属性和原型链上的属性)组成的数组
Object.geyOwnPropertySymbols:返回一个给定对象的所有 Symbol 值的属性(包括不可枚举的 Symbol 值属性)。
var obj = {};
var a = Symbol("a");
var b = Symbol.for("b");
var c = Symbol.for("c");
obj[a] = "SymbolA";
obj[b] = "SymbolForB";
obj[c] = "SymbolForC";
obj["d"] = "d";
console.log(Object.keys(obj)); // ['d']
console.log(Object.getOwnPropertyNames(obj)); // ['d']
console.log(Object.getOwnPropertySymbols(obj)); // [Symbol(a), Symbol(b), Symbol(c)]
# 026:window.onload 和 DOMContentLoaded
当 DOMContentLoaded 事件触发时,仅当 DOM 加载完成,HTML 结构被解析完,但不包括样式表,图片,flash。
当 onload 事件触发时,页面上所有的 DOM,样式表,脚本,图片都已经加载完成了。
# 027:valueOf 和 toString
- valueOf
valueOf 方法返回指定对象的原始值。
JavaScript 调用 valueOf() 方法用来把对象转换成原始类型的值(数值、字符串和布尔值)。但是我们很少需要自己调用此函数,valueOf 方法一般都会被 JavaScript 自动调用。
const a = [1, 2, 3];
const b = { test: 1 };
console.log(a.valueOf()); // [1,2,3]
console.log(b.valueOf()); // {test: 1}
- toString
toString 返回一个表示该对象的字符串,当对象表示为文本值或以期望的字符串方式被引用时,toString 方法被自动调用。
const a = [1, 2, 3];
const b = { test: 1 };
console.log(a.toString()); //1,2,3
console.log(b.toString()); // [object Object]
- 区别:valueOf 偏向于运算,toString 偏向于显示。
- 使用运算操作符的情况下,valueOf 的优先级高于 toString。
- 在进行对象转换时,将优先调用 toString 方法,如若没有重写 toString,将调用 valueOf 方法;如果两个方法都没有重写,则按 Object 的 toString 输出。
- 在进行强转字符串类型时优先调用 toString 方法,强转为数字时优先调用 valueOf。
共同的缺点:无法获取 null 和 undefined 的值
class A {
valueOf() {
console.log("valueOf");
return 2;
}
toString() {
console.log("toString");
return "哈哈哈";
}
}
let a = new A();
console.log(String(a)); // toString 哈哈哈
console.log(Number(a)); // valueOf 2
console.log(a + "22"); // valueOf '222'
console.log(a == 2); // valueOf true
console.log(a === 2); // false => (严格等于不会触发隐式转换)
# 相关应用:如何让(a===1&&a===2&&a===3)的值为 true?
在双等于==
的时候,因为涉及隐形类型转换可以通过改写valueOf
来实现。
但在===
时,不会通过 valueOf 进行类型转换,因此可以尝试通过 Object.defineProperty 的 get 去进行改写。
// 让a==1&&a==2&&a==3 => true
const A = function(val) {
this.val = val;
};
A.prototype.valueOf = function() {
console.log("valueOf", this.val);
return this.val++;
};
let a = new A(1);
console.log(a == 1 && a == 2 && a == 3);
// valueOf 1
// valueOf 2
// valueOf 3
// true
// 让a===1&&a===2&&a===3 => true
var val = 1;
Object.defineProperty(window, "a", {
get() {
console.log("get value", this.val);
return this.val++;
},
});
console.log(a === 1 && a === 2 && a === 3);
//get value 1
// get value 2
// get value 3
// true
# 029:柯里化
柯里化:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。
柯里化作用: 参数复用、提前返回 和 延迟执行。
// ES6
function curry(fn, ...args) {
const len = fn.length;
return function(...curryArgs) {
const argArr = [...args, ...currArgs];
if (argArr.length < len) {
return curry.call(this, fn, ...argArr);
} else {
return fn.apply(this, argArr);
}
};
}
// ES5
var curry = function curry(fn, arr) {
arr = arr || [];
return function() {
var args = [].slice.call(arguments);
var arg = arr.concat(args);
return arg.length >= fn.length ? fn.apply(this, arg) : curry(fn, arg);
};
};
# 030:实现一个 add 方法
// 实现一个 add 方法,使计算结果能够满足如下预期:add(1)(2)(3)()=6`,`add(1,2,3)(4)()=10
function add1(...args) {
let allArgs = [...args];
const fn = function(...addArgs) {
// 没有参数时进行求和
if (addArgs.length === 0) {
return allArgs.reduce((prev, cur) => prev + cur, 0);
}
// 有参数时push进args数组
allArgs = allArgs.concat(addArgs);
return fn;
};
return fn;
}
// test
console.log(add1(1)(2)(3)());
console.log(add1(1, 2, 3)(4)());
// 在Function需要转换为字符串时,通常会自动调用函数的 toString 方法,因此也可以尝试通过toString方法实现
// 这个方式还兼容支持,add(1)(2)(3)=6, add(1,2,3)(4)=10
function add2(...args) {
let sum = args.length > 0 ? .reduce((prev, cur) => prev + cur, 0) : 0;
const fn = function(...addArgs) {
sum = addArgs.length > 0 ? addArgs.reduce((prev, cur) => prev + cur, sum) : sum;
return fn;
};
fn.toString = function() {
return sum;
};
return fn;
}
// test
console.log(add2(1)(2)(3)()); // f 6
console.log(add2(1, 2, 3)(4)); // f 10
console.log(add2(1, 2, 3)(4)()); // f 10
let val = add2(2)(3);
val = val(4);
console.log(val, val.toString(), typeof val); // f 9 , 9 , funtion
// add3
function add3(...args) {
let allArgs = [...args];
const fn = function(...addArgs) {
allArgs = allArgs.concat(addArgs);
return fn;
}
fn.toString = function() {
if(allArgs.length === 0) {
return 0;
}
return allArgs.reduce((prev, cur) => prev + cur, 0)
}
return fn;
}
# 031:实现 String 的 indexOf 方法
String.prototype.myIndexOf = function(str) {
let sourceArr = this.split("");
let num = -1;
const firstStr = str[0];
for (let i = 0; i < sourceArr.length; i++) {
if (sourceArr[i] === firstStr) {
if (str === this.slice(i, i + str.length)) {
num = i;
break;
}
}
}
return num;
};
# 032:类数组转数组的方法