吱不吱一
箭头函数能当构造函数吗?
箭头函数常用于回调函数中,包括事件处理器或定时器
没有 this
、没有 super
,没有 arguments
,没有 new.target
不能通过 new
关键字调用
一个函数内部有两个方法:[[Call]]
和 [[Construct]]
,在通过 new
进行函数调用时,会执行 [[construct]]
方法,创建一个实例对象,然后再执行这个函数体,将函数的 this
绑定在这个实例对象上
当直接调用时,执行 [[Call]] 方法,直接执行函数体
箭头函数在设计时就没有[[Construct]]内部方法,本身不是一个构建器,不能被用作构造函数调用,当使用 new 进行函数调用时会报错。
URL编码的作用
URL编码(也称为百分比编码)的作用主要是使得在URI(Uniform Resource Identifiers,统一资源标识符)中可以安全且准确地传输数据。由于URI有一套特定的字符集来标识互联网资源,这限制了可以直接在URL中使用的字符。URL编码解决了以下问题:
保留字符的冲突:URL有一些保留字符,比如?、&、/、:等,它们在URL中有特殊的意义。例如,?用于分隔URL的路径和查询字符串,&用于分隔查询字符串中的多个参数。如果这些字符在数据值中出现,就可能被错误地解释为URL的一部分,从而破坏URL的结构。URL编码通过替换这些字符的实例来避免这种冲突。
不安全字符的处理:一些字符,如空格、<、>、"、#等,可能在传输过程中引起歧义或者在某些上下文中不安全。例如,空格在URL中通常会被浏览器转换为+或%20。
非ASCII字符的表示:URL标准只支持ASCII字符集。对于非ASCII字符(如中文、阿拉伯文、表情符号等),URL编码提供了一种方法来表示这些字符。它们被转换成字节序列(基于某种字符编码,如UTF-8),然后每个字节都表示为%后跟两个十六进制数。
完整性和一致性:通过确保URL结构的完整性和传输数据的一致性,URL编码使得数据在不同的计算机和网络设备之间可以可靠地传输和接收
我们知道使用的URL传参格式为: ?name1=value1&name2=value2
,这样在服务端在收到这种字符串的时候,会用“&”分割出每一个参数,然后再用“=”来分割出参数值
但是如果我的参数名或才参数恰好包含了 &
=
等这样的特殊字符怎么办?比如 name1=va&lu=e1
,这里我想传的 name1
= va&lu=e1
的值,那么实际在传输过程中就会变成这样 name1=va&lu=e1
如何进行URL编码
URL编码只是简单的在特殊字符的各个字节前加上 %
,后面跟随两个表示该字符ASCII码(对于非ASCII字符,先转换为字节序列)的十六进制值
例如对上述的字符进行URL编码后结果:“name1=va%26lu%3D”,这样服务端会把紧跟在 %
后的字节当成普通的字节,就是不会把它当成各个参数或键值对的分隔符。
decodeURI与decodeURIComponent区别
encodeURI
函数用于对完整的URI进行编码,它不会编码属于URI一部分的特殊字符:, / ? : @ & = + $ #
。这意味着,使用encodeURI编码一个完整的URL时,这些字符保持不变,因为它们在URL结构中扮演特定的角色
let url = "http://example.com/路径?query=测试&sort=asc";
let encodedUrl = encodeURI(url);
console.log(encodedUrl);
// 输出:"http://example.com/%E8%B7%AF%E5%BE%84?query=%E6%B5%8B%E8%AF%95&sort=asc"
// 可以看到,路径中的非ASCII字符("路径"和"测试")被编码了,但URL的结构(如?和&)保持不变。
而 encodeURIComponent()
则会对它发现的任何非标准字符进行编码,包括分隔URI各个部分的特殊字符:, / ? : @ & = + $ #
。这使得 encodeURIComponent
非常适合编码URL的键值对,如查询参数的名称和值
let paramKey = "query";
let paramValue = "测试/值";
let encodedKey = encodeURIComponent(paramKey);
let encodedValue = encodeURIComponent(paramValue);
console.log(`${encodedKey}=${encodedValue}`);
// 输出:"query=%E6%B5%8B%E8%AF%95%2F%E5%80%BC"
选择 encodeURI
还是 encodeURIComponent
取决于你需要编码的是整个URL还是URL的某个部分(如查询字符串参数)。如果你想保持URL的结构,使用 encodeURI
;如果你需要编码 URL 的某一部分,如参数的名称和值,使用 encodeURIComponent
以确保特殊字符也被编码,避免URL解析错误
forEach和map的区别
forEach
用途:
forEach
用于遍历数组中的每个元素并对每个元素执行回调函数。它主要用于执行副作用返回值:
forEach
没有返回值(或者说返回undefined)修改原数组:回调函数中对元素的任何修改都会影响原数组的元素,但
forEach
本身不返回任何值map
: 会分配内存空间存储新数组并返回,forEach()
不会返回数据
map
用途:
map
用于遍历数组中的每个元素,对每个元素执行回调函数,并将回调函数的返回值组成一个新数组返回。它主要用于不改变原数组的情况下,根据原数组创建一个新的数组返回值:
map
返回一个新数组,该数组包含回调函数返回的结果。修改原数组:
map
不直接修改原数组,但如果回调函数内部执行了修改原数组元素的操作,那么原数组还是会被修改。
var a = [1,3]
a.map(item => item = item+1) // [2,4]
console.log(a) // [1,3]
for, forEach,map的性能
for > forEach > map
for
循环是最简单的,因为它没有任何额外的函数调用栈和上下文forEach
其次,因为它其实比我们想象得要复杂一些,它的函数签名实际上是array.forEach(function(currentValue, index, arr), thisValue)
它不是普通的
for
循环的语法糖,还有诸多参数和上下文需要在执行的时候考虑进来,这里可能拖慢性能map
最慢,因为它的返回值是一个等长的全新的数组,数组创建和赋值产生的性能开销很大
for...of可以用在对象中么
答案是不行
for...of
语句只用在可迭代对象,即实现了[Symbol.iterator]
方法的对象,比如Array
,Map
,Set
,String
,TypedArray
,arguments
对象等等)
现可迭代协议的对象:
var iterable = {
[Symbol.iterator]() {
return {
i: 0,
next() {
if (this.i < 3) {
return { value: this.i++, done: false };
}
return { value: undefined, done: true };
}
};
}
};
for (var value of iterable) {
console.log(value);
}
indexOf和includes
indexOf
:可返回某个指定的字符串值在字符串中首次出现的位置。是ES5的方法,也可以对字符串使用
对大小写敏感。
它内部使用相等运算符(===)进行判断,这会导致对
NaN
的误判
includes
:- 检查数组或字符串是否包含某些元素,返回
true
或false
,是ES6的方法,也可以对字符串使用
- 检查数组或字符串是否包含某些元素,返回
[1, NaN].includes(NaN) // true
[1, NaN].indexOf(NaN) // -1
他们对 NaN 的处理是不一样
ES6新增数组遍历方法
Array.prototype.find()
用于找出数组中满足提供的测试函数的第一个元素的值。 如果没有找到满足条件的元素,则返回undefined。
let arr = [1, 5, 10, 15]; let found = arr.find(element => element > 9); // 返回10
Array.prototype.findIndex()
类似于find,但返回满足提供的测试函数的第一个元素的索引,而不是元素的值。 如果没有找到满足条件的元素,则返回-1。
let arr = [1, 5, 10, 15]; let foundIndex = arr.findIndex(element => element > 9); // 返回2
Array.prototype.includes()`
用于判断数组是否包含一个指定的值,根据情况返回
true
或false
。 与indexOf不同,includes直接返回布尔值,更直观。let arr = [1, 2, 3]; console.log(arr.includes(2)); // true console.log(arr.includes(4)); // false
Array.prototype.keys()
返回一个包含数组中每个索引键的Array Iterator对象。
let arr = ['a', 'b', 'c'];
let iterator = arr.keys();
for (let key of iterator) {
console.log(key); // 输出:0 1 2
}
Array.prototype.values()
返回一个新的Array Iterator对象,包含数组每个索引的值。
let arr = ['a', 'b', 'c'];
let valueIterator = arr.values();
for (let value of valueIterator) {
console.log(value); // 输出:'a' 'b' 'c'
}
Array.prototype.entries()
返回一个新的Array Iterator对象,它包含数组中每个索引的键/值对。
let arr = ['a', 'b', 'c'];
let entriesIterator = arr.entries();
for (let entry of entriesIterator) {
console.log(entry); // 输出:[0, 'a'] [1, 'b'] [2, 'c']
}
TIP
实际上,some(), every(), map(), forEach(), 和 reduce() 这些数组方法并非ES6(ECMAScript 2015)新增的。它们都是在之前的ECMAScript版本中引入的
柯里化
Currying 为实现多参函数提供了一个递归降解的实现思路——把接受多个参数的函数变换成接受一 个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数, 在某些编程语言中(如 Haskell),是通过 Currying 技术支持多参函数这一语言特性的。
如下例子:
function add(x, y) {
return x+y
}
// 柯里化函数
function curringAdd(x){
return function(y) {
return x+y
}
}
function addd(x, y, z) {
return x+y+z
}
// 柯里化函数
function curringAddd(x){
return function(y) {
return function (z) {
return x+y+z
}
}
}
对 curriedAdd
进行抽象,可能会得到如下函数 currying
function currying(fn, ...arg1) {
if(arg1.length >= fn.length){
return fn(...arg1)
}
return (...arg2) => {
return currying(fn, ...arg1, ...arg2)
}
}
// 对add进行转换
currying(add, 1)(2)
// 对addd进行转换
currying(add, 1)(2)(3)
// 或者
currying(add, 1)(2, 3)
currying的使用场景
参数复用
固定不变的参数,实现参数复用是 Currying
的主要用途之一,如下:
const base = currying(add, 10)
// 复用第一个参数10
base(10) // 20
base(15) // 25
为什么0.1 + 0.2 != 0.3,请详述理由
因为JS使用的浮点运行标准是IEEE754(64位),并且只要采用IEEE754的语言都有该问题
原因
计算机在计算的时候是没办法直接按十进制计算的。所以计算机在计算的时候分成的两个部分
选择按照IEEE754转成相应的二进制
对阶运算后,再转回十进制
1.进制转换
0.1的二进制表示为0.0001100110011001100110011001100110011001100110011001101(无限循环)
0.2的二进制表示为0.001100110011001100110011001100110011001100110011001101(无限循环)
2.对阶运算
对阶运算这部分也可能产生精度损加上之前转二进制时精度就发生了变化,所以对阶运算后结果得到的结果是
0.0100110011001100110011001100110011001100110011001100
当你在JavaScript(或其他使用IEEE 754标准的编程语言)中尝试进行0.1 + 0.2的运算时,这两个数的二进制近似值被相加,结果是另一个近似值,而不是精确的0.3。因此,你得到的结果是接近但不完全等于0.3的一个数。在JavaScript中,这个结果通常会是0.30000000000000004。
怎么解决精度问题
方式一:使用toFixed()
方法截取位数
parserFloat(0.1+0.2).toFixed(2) // 0.30
方式二:将计算元素都转换成整数,得到结果再除以相应的位数
如 0.1+0.2 => (1+2)/10
其它方式:如果精度要求高,那就使用第三个库Math.js、big.js
其它思考
为什么x=0.1
能等到0.1
标准中规定尾数f的固定长度是52位,再加上省略的一位,这53位是JS精度范围。它最大可以表示2^53(9007199254740992), 长度是 16, 所以可以使用 toPrecision(16) 来做精度运算,超过的精度会自动做凑整处理
(0.1).toPrecision(16) // 0.1000000000000000
(0.1).toPrecision(21) // 0.100000000000000005551
js最大安全数是 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1, 而不是Math.pow(2,52) - 1, why?
这是因为二进制表示有效数字总是1.xx…xx的形式,尾数部分f在规约形式下第一位默认为1(省略不写,xx..xx为尾数部分f,最长52位)。 因此,JavaScript提供的有效数字最长为53个二进制位(64位浮点的后52位+被省略的1位)
总结
精度损失可能出现在进制转化和对阶运算过程中,只要这两步产生了精度损失,计算结果就会出现偏差。
平时工作如果有涉及到浮点数的计算时,一定要注意一下了呐
super的作用
super
表示父类的构造函数.
- 在构造函数中调用父类的构造函数
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类自己的 this
对象必须先通过父类的构造函数完成塑造, 得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this对象
- 在方法中调用父类的方法 super也可以用于派生类方法中,以调用父类上同名的方法。这在需要扩展或修改父类方法的行为时非常有用
如何在 ES5 环境下实现一个const
由于ES5环境没有block的概念,所以是无法百分百实现const,只能是挂载到某个对象下,要么是全局的window,要么就是自定义一个object来当容器
var __const = function __const (data, value) {
window.data = value // 把要定义的data挂载到window下,并赋值value
Object.defineProperty(window, data, { // 利用Object.defineProperty的能力劫持当前对象,并修改其属性描述符
enumerable: false,
configurable: false,
get: function () {
return value
},
set: function (data) {
if (data !== value) { // 当要对当前属性进行赋值时,则抛出错误!
throw new TypeError('Assignment to constant variable.')
} else {
return value
}
}
})
}
__const('a', 10)
console.log(a)
delete a
console.log(a)
for (let item in window) { // 因为const定义的属性在global下也是不存在的,所以用到了enumerable: false来模拟这一功能
if (item === 'a') { // 因为不可枚举,所以不执行
console.log(window[item])
}
}
a = 20 // 报错
图片base64压缩后变大还是变小,如何提高性能的
将小图像直接编码到HTML或CSS中,可以减少HTTP请求的数量,从而提高小资源的加载效率,但是 BASE64 搞出来的图片通常尺寸会大上 30% 左右,这是因为Base64编码方式是将二进制数据转换成ASCII字符串,以便在文本流中传输。在这个过程中,它将每3个字节的二进制数据转换成4个字节的文本格式,从而导致数据膨胀
优化的话:
小图片才用base64
如果 base64 是被编码到 css/js 中,是可以缓存的,因为 css/js 文件可以缓存
移动端事件:click、tap和touch事件之间的区别
在移动端开发中,处理用户交互事件是常见需求,其中 click、tap和touch 事件是最基本的交互形式。这些事件之间有着明显的区别,了解这些区别对于提高应用的响应性和用户体验非常重要。
Click 事件
通用性:click事件不是专门为触摸屏设计的,它是从传统的桌面浏览器继承而来的,用于响应鼠标的点击操作。但在移动设备上,浏览器会将触摸操作解释为click事件。
延迟:为了区分单击与双击操作,移动浏览器通常会在触发click事件前添加大约300毫秒的延迟,这会导致用户感觉到明显的响应延迟。
兼容性:几乎所有浏览器都支持click事件,兼容性最好。
Tap 事件
专门化:tap事件是专门为触摸设备设计的,它旨在提供比click事件更快的响应。tap事件通常由各种JavaScript库实现,例如Zepto和jQuery Mobile。
无延迟:与click事件相比,tap事件没有300毫秒的延迟,可以立即响应用户的触摸操作,从而提高应用的交互性能。
兼容性:tap事件不是所有浏览器的标准部分,它通常需要通过第三方库来实现和使用。
Touch 事件
灵活性:touch事件提供了对触摸操作的底层访问,包括touchstart、touchmove、touchend和touchcancel。这些事件可以用来处理更复杂的触摸行为,如滑动、拖拽和缩放。
即时反应:touch事件允许开发者直接处理触摸的开始和结束,可以用来创建非常流畅和即时响应的用户界面。
注意事项:处理touch事件时,需要小心处理事件的默认行为,以避免例如页面滚动等不期望的行为。同时,touch事件的使用需要考虑到事件处理程序的性能,以免影响用户体验。
总结
选择click:当你需要保证网页在桌面和移动设备上都有良好兼容性时,但可能会遇到300ms延迟问题。
选择tap:当你使用了支持tap事件的库,且想提供快速响应的触摸交互时,但需要引入额外的库。
选择touch:当你需要处理复杂的触摸交互,如滑动、拖拽或缩放,且希望控制触摸操作的每个阶段时。
Q:移动端 300 ms 点击(click 事件)延迟
由于移动端会有双击缩放的这个操作,因此浏览器在 click 之后要等待 300ms,判断这次操作是不是双击。
解决方案:
禁用缩放:user-scalable=no
更改默认的视口宽度
CSS touch-action
Q:点击穿透问题
因为 click 事件的 300ms 延迟问题,所以有可能会在某些情况触发多次事件。
解决方案:
只用
touch
只用
click
判断输出
Q
function Foo(){}
Foo.prototype.constructor === Foo // true
// 以下是为了方便理解这么设计的原因
var f = new Foo()
f.constructor // Foo(){} , 这一步没什么问题指向 f 的构建函数
f.hasOwnProperty('constructor') // fase, 因为 f 本身没有这个 constructor 属性
a.__proto__.hasOwnProperty('constructor') // true, 所以 constructor 在原型对象中
// Foo.prototype.constructor === Foo 是为了实例对象知道自己的构建函数是谁
当你使用 new
关键字创建一个新对象时(比如new Foo()),JavaScript 引擎会使用 Foo 的 prototype
来给新对象设置原型链。这意味着新对象可以访问 Foo.prototype
中定义的所有属性和方法。通过 prototype
对象的 constructor
属性,新对象可以知道自己是由哪个构造函数创建的。
Q
if (!("a" in window)) {
var a = 1;
}
alert(a) // undefined
注意点:
所有的变量声明都在范围作用域的顶部
alert("a" in window); var a // 等价于 var a alert("a" in window);
变量声明被提前了,但变量赋值没有,因为这行代码包括了变量声明和变量赋值,进入执行上下文会声明这些变量(除了函数声明),运行执行上下文才会对变量赋值
所有的全局变量都是
window
的属性,语句var a = 1
;等价于window.a = 1
Q
var a = 1,
b = function a(x) {
x && a(--x);
};
alert(a) // 1
题目注意点:
变量提升中,函数声明会覆盖同名的变量
变量提升中,声明及函数表达式不会覆盖之前的同名变量
命名表达式的名字只在新定义的函数作用域内有效,因为规范规定了标示符不能在外围的作用域内有效
题目中的函数表达式 a
不存在于全局上下文的,也就不会覆盖之前 var a
,代码等价于下面的代码:
var a = 1,
b = function(x) {
x && b(--x);
};
alert(a)
Q
function value(){
return 1;
}
var value;
alert(typeof value); //"function"
注意点:
变量提升中,函数声明会覆盖,同名的变量
变量提升中,声明及函数表达式不会覆盖之前的同名变量
function value(){
return 1;
}
var value = 1;
alert(typeof value); //"number"
注意点:
变量提升中,函数声明会覆盖,同名的变量
变量提升中,声明及函数表达式不会覆盖之前的同名变量
当进入变量赋值阶段 value
被生新定义,上面代码等价于:
function value(){
return 1;
}
value = 1;
下面这个代码运行结果是一样的
var value = 1;
function value(){
return 1;
}
alert(typeof value) // //"number"
Q
function a(x) {
return x * 2;
}
var a;
alert(a); // function a ()
注意点:
- 变量声明不会影响同名的变量
Q
function b(x, y, a) {
arguments[2] = 10;
alert(a); // 10
}
b(1, 2, 3)
活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments
属性初始化。arguments
属性的值是Arguments
对象:
AO = {
arguments: <ArgO>
}
Arguments
对象是活动对象的一个属性,它包括如下属性:
callee
— 指向当前函数的引用length
— 真正传递的参数个数properties-indexes
(字符串类型的整数) 属性的值就是函数的参数值(按参数列表从左到右排列).properties-indexes
内部元素的个数等于arguments.length
.properties-indexes
的值和实际传递进来的参数之间是共享的
这个共享其实不是真正的共享一个内存地址,而是2个不同的内存地址,使用 JavaScript 引擎来保证2个值是随时一样的,当然这也有一个前提,那就是这个索引值要小于你传入的参数个数,也就是说如果你只传入2个参数,而还继续使用 arguments[2]
赋值的话,就会不一致,例如:
function b(x, y, a) {
arguments[2] = 10;
alert(a); // undefined
}
b(1, 2)
此时 arguments.length
其实是 2 个, arguments[2]
与参数 a
不是一个东西,所以赋值 10
以后, alert(a)
的结果依然是 undefined
,而不是 10
, 但如下代码弹出的结果依然是 10
,因为和 a
没有关系
function b(x, y, a) {
arguments[2] = 10;
alert(arguments[2]); // 10
}
b(1, 2)
Q
var b = 10;
(function b(){
b = 20;
console.log(b);
})();
输出
ƒ b(){
b = 20;
console.log(b);
}
说明:
立即执行函数表达式(IIFE):
function b(){...}()
是一个立即执行的函数表达式。注意,这里的function b(){...}
是一个函数表达式而不是函数声明,因为它是在括号内部。函数表达式可以有名称,但该名称只在函数内部可见,即它创建了一个名为b
的局部绑定。当执行
b = 20
;这行代码试图修改b的值时,实际上是试图改变这个指向函数本身的引用。但是,由于在函数体内部,函数名称(这里是b)是只读的,这意味着你不能将它重新赋值为其他内容(如数字20)。因此,这个赋值操作不会有任何效果,b 仍然指向那个函数本身。
扩展
var b = 10;
(function b() {
window.b = 20; //
console.log(b); // [Function b]
console.log(window.b); // 20
})();
Q
var obj = {
'2': 3,
'3': 4,
'length': 2,
'splice': Array.prototype.splice,
'push': Array.prototype.push
}
obj.push(1)
obj.push(2)
console.log(obj)
输出:[,,1,2] length 为 4
解释:Array.prototype.push
将根据 length
将元素填充到对应位置并修改 length
属性 +1
,所以输出的结果就是上述结果。
Q
function a() {
alert(this); // [object Window]
}
a.call(null)
如果 call
第一个参数传入的对象调用者是 null
或者 undefined
的话,call
方法将把全局对象(也就是 window
)作为 this
的值。所以,不管你什么时候传入 null
,其 this
都是全局对象 window
,所以该题目可以理解成如下代码:
function a() {
alert(this);
}
a.call(window);
Q:下面代码中 a 在什么情况下会打印 1
var a = ?;
if(a == 1 && a == 2 && a == 3){
conso.log(1);
}
var a = {
i: 1,
toString() {
return a.i++;
}
}
if( a == 1 && a == 2 && a == 3 ) {
console.log(1);
}
Q: 输出以下代码的执行结果并解释为什么
var a = {n: 1};
var b = a;
a.x = a = {n:2} ;
console.log(a.x) // undefined
console.log(b.x) // {n: 2}
a和b同时引用了{n:1}对象
接着执行到
a.x = a = {n:2}
语句,尽管赋值是从右到左的没错,但是.
的优先级比=
要高,所以这里首先执行a.x
, 相当于为a
(或者b
)所指向的{n:1}
对象新增了一个属性x
,即此时对象将变为{n:1;x:undefined}之后按正常情况,从右到左进行赋值,此时执行
a ={n:2}
的时候,a
的引用改变,指向了新对象{n:2},而b依然指向的是旧对象
自声明函数
一般是在函数内部,重写同名函数代码,比如:
var scareMe = function () {
alert("Boo!");
scareMe = function () {
alert("Double boo!");
};
};
运行结果:
// 1. 添加新属性
scareMe.property = "properly";
// 2. scareMe赋与一个新值
var prank = scareMe;
// 3. 作为一个方法调用
var spooky = {
boo: scareMe
};
// 使用新变量名称进行调用
prank(); // "Boo!"
prank(); // "Boo!"
console.log(prank.property); // "properly"
// 使用方法进行调用
spooky.boo(); // "Boo!"
spooky.boo(); // "Boo!"
console.log(spooky.boo.property); // "properly"
scareMe(); // Double boo!
scareMe(); // Double boo!
console.log(scareMe.property); // undefined
Q
判断输出
var a = 10;
(function () {
console.log(a) // undefine
a = 5
console.log(window.a) // 10
var a = 20;
console.log(a) // 20
})()
A
这个要注意自执行函数的这行代码var a = 20
因为这里又声明一个a
,导致这个变量提升,所在当前块级作用域中且在var a=20
之前访问的a
将为undefine
如果没有var a = 20
,那么将访问全局的变量window.a
var a = 10;
(function () {
console.log(a) // 10
a = 5
console.log(window.a) // 5
a = 20;
console.log(a) // 20
})()
[1 < 2 < 3, 3 < 2 < 1]
解析:这个题会让人误以为是 2 > 1 && 2 < 3 其实不是的.
这个题等价于
1 < 2 => true;
true < 3 => 1 < 3 => true;
3 < 2 => false;
false < 1 => 0 < 1 => true;
答案是 [true, true]
Q
2 == [[[2]]]
解析: [[[2]]]
应该会不断得使用 toString()
做类型转换, 最终 [[[2]]] => '2'
'2'==2
答案 true
Q
var a = new Date("2014-03-19"),
b = new Date(2014, 03, 19);
[a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]
在JavaScript中,Date对象的月份是从0开始计数的,即0表示一月,1表示二月,依此类推,直到11表示十二月。因此,当你创建Date对象时,需要特别注意月份的指定。
对于变量a,使用的是日期字符串构造,其中月份03按照常规理解是三月,所以a代表的是2014年3月19日。
对于变量b,直接使用年、月、日的数字构造,但这里的月份是从0开始的,所以03实际代表四月,因此b代表的是2014年4月19日。
现在,考虑表达式 [a.getDay() === b.getDay(), a.getMonth() === b.getMonth()]
getDay()
方法返回的是星期几,范围是0(周日)到6(周六)。由于a和b都是在19号,但由于它们分别在三月和四月,而这两个月的19号可能是不同的星期几,所以这部分的结果取决于具体的年月。在这个例子中,2014-03-19是星期三,2014-04-19也是星期六,因此a.getDay() === b.getDay()的结果为false。
getMonth()
方法返回的是月份,从0开始计数。由于a是三月(2),b是四月(3),所以 a.getMonth() === b.getMonth()
的结果为 false
。
答案 [false, false]
Q
3.toString() // 报错
3..toString() // '3'
3...toString() // 报错
解析:
- 因为当原始数据类型(boolean,Number、String)在调用方法时,JS 将会创建对象,以便调用方法属性,而在使用完毕后将会销毁该对象
3.toString()
:这是因为javascript引擎在解释代码1.toString()
时认为.
是浮点符号,此时但因小数点后面的字符是非法的,所以报语法错误3..toString()
: javascript引擎认为第一个.
是表示小数点,(JS 中1.
、.1
这样形式是合法),之后第二个.
被解析为属性访问语法,所以都能正确解释执行3...toString()
: 继上面之后属性访问语法访问的是一个.
,所以报语法错误
Q
(function(){
var x = y = 1;
})();
console.log(y);
console.log(x);
解析:
var x
:x
是函数作用域y
: 没有var
,那么将变成window.y
答案是 1, error
Q
var a = /123/,
b = /123/;
a == b
a === b
解析:正则是引用类型
typeof a // object
答案 false
, false
Q
var a = [1, 2, 3],
b = [1, 2, 3],
c = [1, 2, 4]
a == b
a === b
a > c
a < c
解析:前面两个是引类型的判断,不会相等,后面两个在比较大小的时候会 toString()
后按照字典序比较
[1, 2, 3].toString() === '1,2,3'
[1, 2, 4].toString() === '1,2,4'
'1,2,3' < '1,2,4' // true
答案 false
, false
, false
, true
Q
var a = {}, b = Object.prototype;
[a.prototype === b, Object.getPrototypeOf(a) === b]
解析:
只有函数才拥有
prototype
的属性. 所以a.prototype
为undefined
Object.getPrototypeOf(obj)
相当于obj.__proto__
答案 false
, true
Q
function foo() { }
var oldName = foo.name;
foo.name = "bar";
[oldName, foo.name]
解析: name
是函数本来就有的属性,且名字不可变.
答案 ['foo', 'foo']
Q
"1 2 3".replace(/\d/g, parseInt)
str.replace(regexp|substr, newSubStr|function)
, 如果 replace
函数传入的第二个参数是函数, 那么这个函数将接受如下参数
match
: 首先是匹配的字符串p1, p2 ....
: 正则的分组offset
:match
匹配的index
string
: 整个字符串
由于题目中的正则没有分组, 所以等价于问
parseInt
函数可解析一个字符串,并返回一个整数。第二个参数表示要解析的数字的基数。该值介于 2 ~ 36 之间,具体表示规则如下:
如果省略该参数或其值为 0,则数字将以 10 为基础来解析
如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回
NaN
parseInt('1', 0)
parseInt('2', 2)
parseInt('3', 4)
答案: 1, NaN, 3
Q
var lowerCaseOnly = /^[a-z]+$/;
[lowerCaseOnly.test(null), lowerCaseOnly.test()]
解析:这里 test
函数会将参数转为字符串 null=>'nul'
和 undefined=>'undefined'
自然都是小写了
答案: true, true
Q
[,,,].join(", ")
答案: ", , "
Q
var a = Function.length,
b = new Function().length
a === b
解析:这题有点陷阱
函数的 length
返回该函数接收参数的个数,注意上面的 Function
是一个构造函数,默认是有定义接收一个参数的,所以 Function.length=1
而下面 new Function()
返回的一个新的函数是没有定义接口参数的所以 new Function().length = 0
答案 false
Q
var a = Date(0);
var b = new Date(0);
var c = new Date();
[a === b, b === c, a === c]
Date(0) // "Thu Feb 25 2021 16:05:15 GMT+0800 (中国标准时间)"
new Date(0) // Thu Jan 01 1970 08:00:00 GMT+0800 (中国标准时间)
new Date() // Thu Feb 25 2021 16:04:49 GMT+0800 (中国标准时间)
关于 Date
的题, 需要注意的是
如果不传参数等价于当前时间.
如果是函数调用返回一个字符串.
答案 false
, false
, false
Q
var min = Math.min(), max = Math.max()
min < max
解析:Math.min
不传参数返回 Infinity
, Math.max
不传参数返回 -Infinity
答案: false
Q
Number.MIN_VALUE > 0 // true
解析: MIN_VALUE
属性是 JavaScript 中可表示的最小的数(接近 0 ,但不是负数),它的近似值为 5 x 10-324
Q
var x = [].reverse;
x();
解析:
将
reverse
赋值给x
在全局环境调用
x
,相当于执行window.reverse
答案 报错
Q
["1", "2", "3"].map(parseInt)
解析:
map的用法:
var new_array = arr.map(function callback(currentValue[, index[, array]]) {
// Return element for new_array
}[, thisArg])
这个 callback
一共可以接收三个参数,其中第一个参数代表当前被处理的元素,而第二个参数代表该元素的索引
parseInt的用法:
parseInt()
函数解析一个字符串参数,并返回一个指定基数的整数 (数学系统的基础)。
const intValue = parseInt(string[, radix]);
string:要被解析的值。如果参数不是一个字符串,则将其转换为字符串(使用 ToString 抽象操作)。字符串开头的空白符将会被忽略。
radix :一个介于2和36之间的整数(数学系统的基础),表示上述字符串的基数。默认为10。 返回值 返回一个整数或NaN
parseInt(100); // 100
parseInt(100, 10); // 100
parseInt(100, 2); // 4 -> converts 100 in base 2 to base 10
注意:
在radix
为undefined
,或者radix
为 0 或者没有指定的情况下,JavaScript 作如下处理:
如果字符串 string 以"0x"或者"0X"开头, 则基数是16 (16进制).
如果字符串 string 以"0"开头, 基数是8(八进制)或者10(十进制),那么具体是哪个基数由实现环境决定。ECMAScript 5 规定使用10,但是并不是所有的浏览器都遵循这个规定。因此,永远都要明确给出radix参数的值。
如果字符串 string 以其它任何值开头,则基数是10 (十进制)。
回到题目
['1', '2', '3'].map(parseInt)
等价于:
['1', '2', '3'].map((item, index) => {
return parseInt(item, index)
})
即返回的值分别为:
parseInt('1', 0) ,
radix
为0,默认为10,所以返回1parseInt('2', 1),
radix
为1, 不在2到36之间,所以返回NaN
parseInt('3', 2) ,
radix
为2,将'3'作为二进制处理,逢2进1,可'3'不是二进制表示(已经超过最大值1了),所以返回NaN
变种['10','10','10','10','10'].map(parseInt)
分析如一:
parseInt('10', 0) ,
radix
为0,默认为10,所以返回10parseInt('10', 1) ,
radix
为1, 不在2到36之间,所以返回NaN
parseInt('10', 2) ,
radix
为2,将'10'作为二进制处理,逢2进1,返回2parseInt('10', 3) ,
radix
为3,逢3进1,所以返回3parseInt('10', 4) ,
radix
为4,逢4进1,所以返回4
如果想要正常得循环字符串数组,怎么办?
['10','10','10','10','10'].map(Number);
// [10, 10, 10, 10, 10]
Q
[typeof null, null instanceof Object]
解析:
instanceof
只能用于对象,null
是基本类型作为基本类型的
null
,typeof null=object
这点确实让人很困惑,不过这是历史设计如此,硬记~
所以答案 [object, false]
Q
var val = 'smtg';
console.log('Value is ' + (val === 'smtg') ? 'Something' : 'Nothing');
解析: +
的优先级大于 ?
所以原题等价于 'Value is true' ? 'Somthing' : 'Nonthing'
而不是 'Value is' + (true ? 'Something' : 'Nonthing')
答案: Something
Q
var name = 'World!';
(function () {
if (typeof name === 'undefined') {
var name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
解析:考变量提升,代码等价于:
var name = 'World!';
(function () {
var name;
if (typeof name === 'undefined') {
name = 'Jack';
console.log('Goodbye ' + name);
} else {
console.log('Hello ' + name);
}
})();
所以答案是 'Goodbye Jack'
Q
var ary = [0,1,2];
ary[10] = 10;
ary.filter(function(x) { return x === undefined;});
解析:使用 filter
迭代这个数组的时候, 首先检查了这个索引值是不是数组的一个属性,也就是说 从 3 - 9
的这些索引并不存在与数组中. 在 array
的函数调用的时候是会跳过这些不存的索引
答案是 []
Q
function showCase(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase(new String('A'));
解析: switch
是严格比较, String 实例
和 字符串
不一样
答案是 'Do not know!'
Q
function showCase2(value) {
switch(value) {
case 'A':
console.log('Case A');
break;
case 'B':
console.log('Case B');
break;
case undefined:
console.log('undefined');
break;
default:
console.log('Do not know!');
}
}
showCase2(String('A'));
解析: String
作为普通函数调用直接调用返回一个字符串
答案 'Case A'
Q
Array.isArray( Array.prototype )
解析:一个鲜为人知的实事: Array.prototype => []
答案: true
Q
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1)
arguments
是一个特殊引用关系的 object
, c
就是 arguments[2]
, 所以对于 c
的修改就是对 arguments[2]
的修改
所以答案是 21
function sidEffecting(ary) {
ary[0] = ary[2];
}
function bar(a,b,c=3) {
c = 10
sidEffecting(arguments);
return a + b + c;
}
bar(1,1,1)
当给参数添加默认值后, arguments
和形参将脱离
答案是: 12
Q
判断输出:
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log('a'))
console.log('f')
} )
document.body.addEventListener('click', () => {
Promise.resolve().then(() => console.log('a1'))
console.log('f1')
})
上面输出:
f
a
f1
a1
解释应该是用户点击事件的回调是宏任务
第一个注册事件触发后将执行 console.log('f')
,然后清理微任务队列中的 console.log('a'))
, 再执行另一个宏任务事件,执行 console.log('f1')
, 然后再清理这里的微任务队列中的 console.log('a1')
等价于以下代码:
setTimeout(() => {
Promise.resolve().then(() => console.log('a'))
console.log('f')
})
setTimeout(() => {
Promise.resolve().then(() => console.log('a1'))
console.log('f1')
})
如果执行以下代码呢?
document.body.click()
输出结果:
f
f1
a
a1
因为 document.body.click()
是以同步的方式执行的,所以两个回调也是以同步的形式执行。脑阔疼~
为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?
没有跨域问题,一般这种上报数据,代码要写通用的;(排除ajax)
不会阻塞页面加载,影响用户的体验,只要new Image对象就好了;(排除JS/CSS文件资源方式上报)
在所有图片中,体积最小;(比较PNG/JPG)
Q: 使用base64图片的弊端是什么
根据 base64 的编码原理,大小比原文件大小大 1/3
尽管图片请求少了,但是 HTML 文件本身尺寸会变大,会影响首屏加载,所以要权衡
代码看起来会有点丑,大量编码字符(当然也可以通过构建工具动态插入)
base64 无法缓存,要缓存只能缓存包含 base64 的文件,比如 HTML 或者 CSS,这相比直接缓存图片要弱很多,一般 HTM 会改动频繁,所以等同于得不到缓存效益