最近我看了 You-Dont-Know-JS 的两个小册,在看书的过程中,为了方便以后索引与更深入的了解,也为了避免遗忘,我对每一册的较为复杂的点做了总结,编辑如下
本文地址: https://blog.zhequtao.com/post/js-puzzles/
types & grammer
-
判断以下结果
var s = 'abc';s[1] = 'B';console.log(s);var l = new String('abc');l[1] = 'B';console.log(l);string及其包装对象 (Boxed Object) 是不可变 (immutable) 类型,因此不能改变它本身(modify in place),所以String的所有方法都是返回一个新的字符串,而不会改变自身。 -
如何逆序一个字符串?
s.split('').reverse().join('') -
接上,为什么不能直接使用
Array.prototype.reverse.call(s)逆序字符串?当一个数组逆序时
l.reverse()会改变 l 本身。正如第一题,string不能改变自身。 -
判断以下结果,为什么会出现这样的情况,如何做出正确的比较?
0.1 + 0.2 === 0.3;0.8 - 0.6 === 0.2;浮点数根据 IEEE 754 标准存储64 bit 双精度,能够表示 2^64 个数,而浮点数是无穷的,代表有些浮点数必会有精度的损失,0.1,0.2 表示为二进制会有精度的损失。比较时引入一个很小的数值
Number.EPSILON容忍误差,其值为2^-52。function equal (a, b) {return Math.abs(a - b) < Number.EPSILON} -
如何判断一个数值为整数?
// ES6Number.isInteger(num);// ES5if (!Number.isInteger) {Number.isInteger = function(num) {return typeof num == "number" && num % 1 == 0;};} -
如何判断一个数值为 +0?
function isPosZero (n) {return n === 0 && 1 / n === Infinity} -
'abc'.toUpperCase()中 ‘abc’ 作为 primitive value,如何访问toUpperCase方法当
primitive value访问属性或者方法时,会自动转化为它的包装对象。另外也可以使用Object.prototype.valueOf()解包装(Unboxing)。 -
判断以下结果 (Boxing Wrappers)
function foo() {console.log(this)}foo.call(3);Number(3)。理由如上。
-
判断以下结果
Array.isArray(Array.prototype)true 内置对象的 prototype 都不是纯对象,比如
Date.prototype是 Date,Set.prototype是 Set。 -
判断以下结果
Boolean(new Boolean(false));Boolean(document.all);[] == '';[3] == 3;[] == false;42 == true;new Boolean() 返回 object,为 true document.all,历史问题,参考这里 Falsy value 指会被强制转化为 false 的值,有以下五种。除此之外全部会转化为 true
- undefined
- null
- false
- +0, -0, and NaN
- ""
-
找出以下代码问题 (TDZ)
var a = 3;let a;这是暂时性死域(Temporal Dead Zone)的问题,let a 声明之前,不能使用 a。
-
找出以下代码问题 (TDZ)
var x = 3;function foo (x=x) {// ..}foo()同样,在函数默认参数中,也有 TDZ。
scope & closures
-
var a = 2中,Engine,Scope,Compiler做了什么工作 -
判断以下结果 (Lexical Scope)
var scope = 'global scope';function checkScope () {var scope = 'local scope';function f() {return scope;}return f;}checkScope()();‘local scope’
由于 js 为词法作用域(Lexical Scope),访问某个变量时,先在当前作用域中查找,如果查找不到则在嵌套作用域中查找,直到找到。如果找不到,则报
ReferenceError。 -
判断以下结果 (Hoisting)
console.log(a);var a = 3;undefined
以上代码会被编译器理解为
var a;console.log(a);a = 3; -
判断以下结果 (Function First)
var foo = 1;function foo () {}console.log(foo);1。函数也会有提升,所以会被赋值覆盖。
-
判断以下结果 (IIFE & Function First)
var foo = 1;(function () {foo = 2;function foo () {}console.log(foo);})()console.log(foo);2,1
以上代码会被编译器理解为如下形式
var foo = 1;(function () {var foo;function foo () {}foo = 2;console.log(foo);})()console.log(foo); -
判断以下结果,如何按序输出 (Closure)
for (var i = 0; i < 10; i++) {setTimeout(function () {console.log(i);}, 1000)}大约 1s 之后连续输出 10 个 10。因为没有块级作用域,可以把 var 改成 let,也可以给 setTimeout 包装一层 IIFE。
this & object prototypes
注意:以下均为浏览器环境中
-
判断以下结果 (Default Binding)
function foo() {"use strict";console.log( this.a );}var a = 2;foo();会报错,在函数的严格模式下,默认绑定其中的 this 指向 undefined。
-
判断以下结果
"use strict";var a = 2;let b = 3;console.log(this.a, this.b);2, undefined
在浏览器环境中 this 指向 window,而 var 声明的变量会被挂在 window 上。而 let 声明的变量不会挂在 window 上。
-
判断以下结果 (Strict Mode & Default Binding)
function foo() {console.log( this.a );}var a = 2;(function(){"use strict";foo();})();2
只有存在 this 的函数中设置严格模式,this 为 undefined。因此会正常输出。
-
判断以下结果 (Hard Binding)
function foo () {console.log(this.a);}const o1 = { a: 3 };const o2 = { a: 4 };foo.bind(o1).bind(o2)();3
bind 为硬绑定,第一次绑定后 this 无法再次绑定。
-
如何实现
Function.prototype.bind与Function.prototype.softBindbind 为硬绑定,softBind 可以多次绑定 this。大致实现代码如下。bind 第二个参数可以预设函数参数,所以,bind 也是一个偏函数。另外,bind 也需要考虑
new的情况。但以下示例主要集中在硬绑定和软绑定的差异之上。Function.prototype.fakeBind = function (obj) {var self = this;return function () {self.call(obj);}}Function.prototype.softBind = function(obj) {var self = this;return function () {self.call(this === window? obj : this);}}; -
new的过程中发生了什么,判断以下结果 (new)function F () {this.a = 3;return {a: 4;}}const f = new F();console.log(f.a);4
new的过程大致为如下几个步骤- 创建一个新的对象
- this 指向实例,并且执行函数
- 如果没有显式返回,则默认返回这个实例
因为函数最后显式返回了一个对象,所以打印为 4
-
什么是
data descriptor和accessor descriptor两者均通过
Object.defineProperty()定义,有两个公有的键值- configurable 设置该键是否可以删除
- enumerable 设置是否可被遍历
数据描述符有以下键值
- writable 该键是否可以更改
- value
访问器描述符有以下键值
- set
- get 另外,也可以通过字面量的形式表示访问器描述符
const obj = {get a() {},set a(val) {}}Vue中
computed的内部原理便是get,而watch的内部原理是set -
如何访问一个对象的属性? ([[Get]])
访问对象的属性会触发 [[Get]] 操作,大致简述如下
- 是否被 Proxy 拦截,如果拦截,查看拦截器的返回值,如果没拦截,继续下一步
- 检查自身属性,如果没找到则继续下一步
- 如果没被找到,则在原型链上查找,如果没找到,则返回 undefined
查找过程与 Scope 查找变量很相似,只不过,对象属性找不到,返回 undefined,而变量找不到报 Reference Error。
-
如何对一个对象的属性赋值 ([[Put]])
对一个对象的属性赋值会触发 [[Put]] 操作,大致简述如下
- 检查是否被 Proxy 拦截
- 如果该对象属性为自身属性 (obj.hasOwnProperty(‘a’) === true)
- 如果属性是访问描述符,则调用 setter 函数
- 如果属性是 data descriptor,则检查 writable 是否可写
- 普通属性,直接赋值
- 如果该对象属性存在于原型链上
- 如果属性是访问描述符,则调用 setter 函数
- 如果属性是 data descriptor,则检查 writable 是否可写。如果可写,被自身属性覆盖,否则在严格模式下将会报错
- 普通属性,被自身属性覆盖
- 如果该对象不存在与原型链上,直接给自身属性赋值
-
如何遍历一个对象 ($$iterator)
给对象设置 Symbol.iterator 属性
-
如何实现一个继承 (Object.create & call)
在 ES6 时代可以简单的通过 class & extends 实现继承,ES5 时代用如下方法
function A () {}function B () {A.call(this)}B.prototype = Object.create(A.prototype)// B.prototype = new A() 不推荐 -
如何实现
Object.create至于为什么在继承的时候不推荐
new,原因在于你很难保证 A 是一个纯函数,比如它会有自身属性,有可能操作 DOM 等。以下是一个简单版本的实现,省略了第二个参数。Object.create = function (o) {function F() {}F.prototype = o;return new F();}