最近在整理 JavaScript 的时候发现遇到了很多面试中常见的面试题,本部分主要是作者在 Github 等各大论坛收录的 JavaScript 相关知识和一些相关面试题时所做的笔记,分享这份总结给大家,对大家对 JavaScript 的可以来一次全方位的检漏和排查,感谢原作者 CavsZhouyou 的付出,原文链接放在文章最下方,如果出现错误,希望大家共同指出!
1. 介绍 js 的基本数据类型。
js 一共有六种基本数据类型,分别是 Undefined、Null、Boolean、Number、String,还有在 ES6 中新增的 Symbol 类型,
代表创建后独一无二且不可变的数据类型,它的出现我认为主要是为了解决可能出现的全局变量冲突的问题。
2. JavaScript 有几种类型的值?你能画一下他们的内存图吗?
涉及知识点:
- 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
- 堆:引用数据类型(对象、数组和函数)
两种类型的区别是:存储位置不同。
原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储。
引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。
如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
回答:
js 可以分为两种类型的值,一种是基本数据类型,一种是复杂数据类型。
基本数据类型....(参考1)
复杂数据类型指的是 Object 类型,所有其他的如 Array、Date 等数据类型都可以理解为 Object 类型的子类。
两种类型间的主要区别是它们的存储位置不同,基本数据类型的值直接保存在栈中,而复杂数据类型的值保存在堆中,通过使用在栈中保存对应的指针来获取堆中的值。
3. 什么是堆?什么是栈?它们之间有什么区别和联系?
堆和栈的概念存在于数据结构中和操作系统内存中。
在数据结构中,栈中数据的存取方式为先进后出。而堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。完全
二叉树是堆的一种实现方式。
在操作系统中,内存被分为栈区和堆区。
栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区内存一般由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。
4. 内部属性 [[Class]] 是什么?
所有 typeof 返回值为 "object" 的对象(如数组)都包含一个内部属性 [[Class]](我们可以把它看作一个内部的分类,而非传统的面向对象意义上的类)。这个属性无法直接访问,一般通过 Object.prototype.toString(..) 来查看。例如:
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
5. 介绍 js 有哪些内置对象?
涉及知识点:
全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。
标准内置对象的分类
(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。
例如 Infinity、NaN、undefined、null 字面量
(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。
例如 eval()、parseFloat()、parseInt() 等
(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。
例如 Object、Function、Boolean、Symbol、Error 等
(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。
例如 Number、Math、Date
(5)字符串,用来表示和操作字符串的对象。
例如 String、RegExp
(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array
(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。
例如 Map、Set、WeakMap、WeakSet
(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。
例如 SIMD 等
(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。
例如 JSON 等
(10)控制抽象对象
例如 Promise、Generator 等
(11)反射
例如 Reflect、Proxy
(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。
例如 Intl、Intl.Collator 等
(13)WebAssembly
(14)其他
例如 arguments
回答:
js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。
一般我们经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。
6. undefined 与 undeclared 的区别?
已在作用域中声明但还没有赋值的变量,是 undefined 的。相反,还没有在作用域中声明过的变量,是 undeclared 的。
对于 undeclared 变量的引用,浏览器会报引用错误,如 ReferenceError: b is not defined 。但是我们可以使用 typeof 的安全防范机制来避免报错,因为对于 undeclared(或者 not defined )变量,typeof 会返回 "undefined"。
7. null 和 undefined 的区别?
首先 Undefined 和 Null 都是基本数据类型,这两个基本数据类型分别都只有一个值,就是 undefined 和 null。
undefined 代表的含义是未定义,null 代表的含义是空对象。一般变量声明了但还没有定义的时候会返回 undefined,null主要用于赋值给一些可能会返回对象的变量,作为初始化。
undefined 在 js 中不是一个保留字,这意味着我们可以使用 undefined 来作为一个变量名,这样的做法是非常危险的,它会影响我们对 undefined 值的判断。但是我们可以通过一些方法获得安全的 undefined 值,比如说 void 0。
当我们对两种类型使用 typeof 进行判断的时候,Null 类型化会返回 “object”,这是一个历史遗留的问题。当我们使用双等号对两种类型的值进行比较时会返回 true,使用三个等号时会返回 false。
8. 如何获取安全的 undefined 值?
因为 undefined 是一个标识符,所以可以被当作变量来使用和赋值,但是这样会影响 undefined 的正常判断。
表达式void ___
没有返回值,因此返回结果是 undefined。void 并不改变表达式的结果,只是让表达式不返回值。
按惯例我们用 void 0 来获得 undefined。
9. 说几条写 JavaScript 的基本规范?
在平常项目开发中,我们遵守一些这样的基本规范,比如说:
(1)一个函数作用域中所有的变量声明应该尽量提到函数首部,用一个 var 声明,不允许出现两个连续的 var 声明,声明时如果变量没有值,应该给该变量赋值对应类型的初始值,便于他人阅读代码时,能够一目了然的知道变量对应的类型值。
(2)代码中出现地址、时间等字符串时需要使用常量代替。
(3)在进行比较的时候吧,尽量使用'===', '!=='代替'==', '!='。
(4)不要在内置对象的原型上添加方法,如 Array, Date。
(5)switch 语句必须带有 default 分支。
(6)for 循环必须使用大括号。
(7)if 语句必须使用大括号。
10. JavaScript 原型,原型链? 有什么特点?
在 js 中我们是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性值,这个属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。
当我们使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。
一般来说我们是不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来让我们访问这个属性,但是我们最好不要使用这个属性,因为它不是规范中规定的。
ES5 中新增了一个 Object.getPrototypeOf() 方法,我们可以通过这个方法来获取对象的原型。
当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是我们新建的对象为什么能够使用 toString() 等方法的原因。
特点:
JavaScript 对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
11. js 获取原型的方法?
- p.proto
- p.constructor.prototype
- Object.getPrototypeOf(p)
12. 在 js 中不同进制数字的表示方式
- 以 0X、0x 开头的表示为十六进制。
- 以 0、0O、0o 开头的表示为八进制。
- 以 0B、0b 开头的表示为二进制格式。
13. js 中整数的安全范围是多少?
安全整数指的是,在这个范围内的整数转化为二进制存储的时候不会出现精度丢失,能够被“安全”呈现的最大整数是 2^53 - 1,即9007199254740991,在 ES6 中被定义为 Number.MAX_SAFE_INTEGER。
最小整数是-9007199254740991,在 ES6 中被定义为 Number.MIN_SAFE_INTEGER。
如果某次计算的结果得到了一个超过 JavaScript 数值范围的值,那么这个值会被自动转换为特殊的 Infinity 值。如果某次计算返回了正或负的 Infinity 值,那么该值将无法参与下一次的计算。判断一个数是不是有穷的,可以使用 isFinite 函数来判断。
14. typeof NaN 的结果是什么?
NaN 意指“不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。
typeof NaN; // "number"
NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN != NaN为 true。
15. isNaN 和 Number.isNaN 函数的区别?
函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,这种方法对于 NaN 的判断更为准确。
16. Array 构造函数只有一个参数值时的表现?
Array 构造函数只带一个数字参数的时候,该参数会被作为数组的预设长度(length),而非只充当数组中的一个元素。这样创建出来的只是一个空数组,只不过它的 length 属性被设置成了指定的值。
构造函数 Array(..) 不要求必须带 new 关键字。不带时,它会被自动补上。
17. 其他值到字符串的转换规则?
规范的 9.8 节中定义了抽象操作 ToString ,它负责处理非字符串到字符串的强制类型转换。
(1)Null 和 Undefined 类型 ,null 转换为 "null",undefined 转换为 "undefined",
(2)Boolean 类型,true 转换为 "true",false 转换为 "false"。
(3)Number 类型的值直接转换,不过那些极小和极大的数字会使用指数形式。
(4)Symbol 类型的值直接转换,但是只允许显式强制类型转换,使用隐式强制类型转换会产生错误。
(3)对普通对象来说,除非自行定义 toString() 方法,否则会调用toString()(Object.prototype.toString())
来返回内部属性 [[Class]] 的值,如"[object Object]"。
如果对象有自己的 toString() 方法,字符串化时就会调用该方法并使用其返回值。
18. 其他值到数字值的转换规则?
有时我们需要将非数字值当作数字来使用,比如数学运算。为此 ES5 规范在 9.3 节定义了抽象操作 ToNumber。
(1)Undefined 类型的值转换为 NaN。
(2)Null 类型的值转换为 0。
(3)Boolean 类型的值,true 转换为 1,false 转换为 0。
(4)String 类型的值转换如同使用 Number() 函数进行转换,如果包含非数字值则转换为 NaN,空字符串为 0。
(5)Symbol 类型的值不能转换为数字,会报错。
(6)对象(包括数组)会首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。
为了将值转换为相应的基本类型值,抽象操作 ToPrimitive 会首先(通过内部操作DefaultValue)检查该值是否有valueOf() 方法。如果有并且返回基本类型值,就使用该值进行强制类型转换。如果没有就使用 toString() 的返回值(如果存在)来进行强制类型转换。
如果 valueOf() 和 toString() 均不返回基本类型值,会产生 TypeError 错误。
19. 其他值到布尔类型的值的转换规则?
ES5 规范 9.2 节中定义了抽象操作 ToBoolean,列举了布尔强制类型转换所有可能出现的结果。
以下这些是假值:
- undefined
- null
- false
- +0、-0 和 NaN
- ""
假值的布尔强制类型转换结果为 false。从逻辑上说,假值列表以外的都应该是真值。
20. {} 和 [] 的 valueOf 和 toString 的结果是什么?
{} 的 valueOf 结果为 {} ,toString 的结果为 "[object Object]"
[] 的 valueOf 结果为 [] ,toString 的结果为 ""
21. 什么是假值对象?
浏览器在某些特定情况下,在常规 JavaScript 语法基础上自己创建了一些外来值,这些就是“假值对象”。
假值对象看起来和普通对象并无二致(都有属性,等等),但将它们强制类型转换为布尔值时,结果为 false 最常见的例子是 document.all,它是一个类数组对象,包含了页面上的所有元素,由 DOM(而不是 JavaScript 引擎)提供给 JavaScript 程序使用。
22. ~ 操作符的作用?
~ 返回 2 的补码,并且 ~ 会将数字转换为 32 位整数,因此我们可以使用 ~ 来进行取整操作。
~x 大致等同于 -(x+1)。
23. 解析字符串中的数字和将字符串强制类型转换为数字的返回结果都是数字,它们之间的区别是什么?
解析允许字符串(如 parseInt() )中含有非数字字符,解析按从左到右的顺序,如果遇到非数字字符就停止。而转换(如 Number ())不允许出现非数字字符,否则会失败并返回 NaN。
24. + 操作符什么时候用于字符串的拼接?
根据 ES5 规范 11.6.1 节,如果某个操作数是字符串或者能够通过以下步骤转换为字符串的话,+ 将进行拼接操作。
如果其中一个操作数是对象(包括数组),则首先对其调用 ToPrimitive 抽象操作,该抽象操作再调用 [[DefaultValue]],以数字作为上下文。如果不能转换为字符串,则会将其转换为数字类型来进行计算。
简单来说就是,如果 + 的其中一个操作数是字符串(或者通过以上步骤最终得到字符串),则执行字符串拼接,否则执行数字加法。
那么对于除了加法的运算符来说,只要其中一方是数字,那么另一方就会被转为数字。
25. 什么情况下会发生布尔值的隐式强制类型转换?
(1) if (..) 语句中的条件判断表达式。
(2) for ( .. ; .. ; .. ) 语句中的条件判断表达式(第二个)。
(3) while (..) 和 do..while(..) 循环中的条件判断表达式。
(4) ? : 中的条件判断表达式。
(5) 逻辑运算符 ||(逻辑或)和 &&(逻辑与)左边的操作数(作为条件判断表达式)。
26. || 和 && 操作符的返回值?
|| 和 && 首先会对第一个操作数执行条件判断,如果其不是布尔值就先进行 ToBoolean 强制类型转换,然后再执行条件判断。
对于 || 来说,如果条件判断结果为 true 就返回第一个操作数的值,如果为 false 就返回第二个操作数的值。
&& 则相反,如果条件判断结果为 true 就返回第二个操作数的值,如果为 false 就返回第一个操作数的值。
|| 和 && 返回它们其中一个操作数的值,而非条件判断的结果。
27. Symbol 值的强制类型转换?
ES6 允许从符号到字符串的显式强制类型转换,然而隐式强制类型转换会产生错误。
Symbol 值不能够被强制类型转换为数字(显式和隐式都会产生错误),但可以被强制类型转换为布尔值(显式和隐式结果都是 true )。
28. == 操作符的强制类型转换规则?
(1)字符串和数字之间的相等比较,将字符串转换为数字之后再进行比较。
(2)其他类型和布尔类型之间的相等比较,先将布尔值转换为数字后,再应用其他规则进行比较。
(3)null 和 undefined 之间的相等比较,结果为真。其他值和它们进行比较都返回假值。
(4)对象和非对象之间的相等比较,对象先调用 ToPrimitive 抽象操作后,再进行比较。
(5)如果一个操作值为 NaN ,则相等比较返回 false( NaN 本身也不等于 NaN )。
(6)如果两个操作值都是对象,则比较它们是不是指向同一个对象。如果两个操作数都指向同一个对象,则相等操作符返回 true,否则,返回 false。
29. 如何将字符串转化为数字,例如 '12.3b'?
(1)使用 Number() 方法,前提是所包含的字符串不包含不合法字符。
(2)使用 parseInt() 方法,parseInt() 函数可解析一个字符串,并返回一个整数。还可以设置要解析的数字的基数。当基数的值为 0,或没有设置该参数时,parseInt() 会根据 string 来判断数字的基数。
(3)使用 parseFloat() 方法,该函数解析一个字符串参数并返回一个浮点数。
(4)使用 + 操作符的隐式转换。
详细资料可以参考:《详解 JS 中 Number()、parseInt() 和 parseFloat() 的区别》
30. 如何将浮点数点左边的数每三位添加一个逗号,如 12000000.11 转化为『12,000,000.11』?
function format(number) {
return number && number.replace(/(?!^)(?=(\d{3})+\.)/g, ",");
}
31. 常用正则表达式
(1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
(2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
(3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;
(4)手机号码正则
var regex = /^1[34578]\d{9}$/g;
(5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;
32. 如何实现数组的随机排序?
(1)使用数组 sort 方法对数组元素随机排序,让 Math.random() 出来的数与 0.5 比较,如果大于就返回 1 交换位置,如果小于就返回 -1,不交换位置。
function randomSort(a, b) {
return Math.random() > 0.5 ? -1 : 1;
}
缺点:每个元素被派到新数组的位置不是随机的,原因是 sort() 方法是依次比较的。
(2)随机从原数组抽取一个元素,加入到新数组
function randomSort(arr) {
var result = [];
while (arr.length > 0) {
var randomIndex = Math.floor(Math.random() * arr.length);
result.push(arr[randomIndex]);
arr.splice(randomIndex, 1);
}
return result;
}
(3)随机交换数组内的元素(洗牌算法类似)
function randomSort(arr) {
var index,
randomIndex,
temp,
len = arr.length;
for (index = 0; index < len; index++) {
randomIndex = Math.floor(Math.random() * (len - index)) + index;
temp = arr[index];
arr[index] = arr[randomIndex];
arr[randomIndex] = temp;
}
return arr;
}
// es6
function randomSort(array) {
let length = array.length;
if (!Array.isArray(array) || length <= 1) return;
for (let index = 0; index < length - 1; index++) {
let randomIndex = Math.floor(Math.random() * (length - index)) + index;
[array[index], array[randomIndex]] = [array[randomIndex], array[index]];
}
return array;
}
34. javascript 创建对象的几种方式?
我们一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。
但是我们可以使用函数来进行模拟,从而产生出可复用的对象创建方式,我了解到的方式有这么几种:
(1)第一种是工厂模式。
工厂模式的主要工作原理是用函数来封装创建对象的细节,从而通过调用函数来达到复用的目的。但是它有一个很大的问题就是创建出来的对象无法和某个类型联系起来,它只是简单的封装了复用代码,而没有建立起对象和类型间的关系。
(2)第二种是构造函数模式。
js 中每一个函数都可以作为构造函数,只要一个函数是通过 new 来调用的,那么我们就可以把它称为构造函数。
执行构造函数首先会创建一个对象,然后将对象的原型指向构造函数的 prototype 属性,然后将执行上下文中的 this 指向这个对象,最后再执行整个函数,如果返回值不是对象,则返回新建的对象。
因为 this 的值指向了新建的对象,因此我们可以使用 this 给对象赋值。构造函数模式相对于工厂模式的优点是,所创建的对象和构造函数建立起了联系,因此我们可以通过原型来识别对象的类型。
但是构造函数存在一个缺点就是,造成了不必要的函数对象的创建,因为在 js 中函数也是一个对象,因此如果对象属性中如果包含函数的话,那么每次我们都会新建一个函数对象,浪费了不必要的内存空间,因为函数是所有的实例都可以通用的。
(3)第三种模式是原型模式。
因为每一个函数都有一个 prototype 属性,这个属性是一个对象,它包含了通过构造函数创建的所有实例都能共享的属性和方法。因此我们可以使用原型对象来添加公用属性和方法,从而实现代码的复用。这种方式相对于构造函数模式来说,解决了函数对象的复用问题。
但是这种模式也存在一些问题,一个是没有办法通过传入参数来初始化值,另一个是如果存在一个引用类型如 Array 这样的值,那么所有的实例将共享一个对象,一个实例对引用类型值的改变会影响所有的实例。
(4)第四种模式是组合使用构造函数模式和原型模式。
这是创建自定义类型的最常见方式。因为构造函数模式和原型模式分开使用都存在一些问题,因此我们可以组合使用这两种模式,通过构造函数来初始化对象的属性,通过原型对象来实现函数方法的复用。
这种方法很好的解决了两种模式单独使用时的缺点,但是有一点不足的就是,因为使用了两种不同的模式,所以对于代码的封装性不够好。
(5)第五种模式是动态原型模式。
这一种模式将原型方法赋值的创建过程移动到了构造函数的内部,通过对属性是否存在的判断,可以实现仅在第一次调用函数时对原型对象赋值一次的效果。这一种方式很好地对上面的混合模式进行了封装。
(6)第六种模式是寄生构造函数模式。
这一种模式和工厂模式的实现基本相同,我对这个模式的理解是,它主要是基于一个已有的类型,在实例化时对实例化的对象进行扩展。这样既不用修改原来的构造函数,也达到了扩展对象的目的。它的一个缺点和工厂模式一样,无法实现对象的识别。
嗯我目前了解到的就是这么几种方式。
35. JavaScript 继承的几种实现方式?
我了解的 js 中实现继承的几种方式有:
(1)第一种是以原型链的方式来实现继承,但是这种实现方式存在的缺点是,在包含有引用类型的数据时,会被所有的实例对象所共享,容易造成修改的混乱。还有就是在创建子类型的时候不能向超类型传递参数。
(2)第二种方式是使用借用构造函数的方式,这种方式是通过在子类型的函数中调用超类型的构造函数来实现的。
这一种方法解决了不能向超类型传递参数的缺点,但是它存在的一个问题就是无法实现函数方法的复用,并且超类型原型定义的方法子类型也没有办法访问到。
(3)第三种方式是组合继承,组合继承是将原型链和借用构造函数组合起来使用的一种方式。通过借用构造函数的方式来实现类型的属性的继承,通过将子类型的原型设置为超类型的实例来实现方法的继承。
这种方式解决了上面的两种模式单独使用时的问题,但是由于我们是以超类型的实例来作为子类型的原型,所以调用了两次超类的构造函数,造成了子类型的原型中多了很多不必要的属性。
(4)第四种方式是原型式继承,原型式继承的主要思路就是基于已有的对象来创建新的对象,实现的原理是,向函数中传入一个对象,然后返回一个以这个对象为原型的对象。
这种继承的思路主要不是为了实现创造一种新的类型,只是对某个对象实现一种简单继承,ES5 中定义的 Object.create() 方法就是原型式继承的实现。缺点与原型链方式相同。
(5)第五种方式是寄生式继承,寄生式继承的思路是创建一个用于封装继承过程的函数,通过传入一个对象,然后复制一个对象的副本,然后对象进行扩展,最后返回这个对象。这个扩展的过程就可以理解是一种继承。这种继承的优点就是对一个简单对象实现继承,如果这个对象不是我们的自定义类型时。缺点是没有办法实现函数的复用。
(6)第六种方式是寄生式组合继承,组合继承的缺点就是使用超类型的实例做为子类型的原型,导致添加了不必要的原型属性。寄生式组合继承的方式是使用超类型的原型的副本来作为子类型的原型,这样就避免了创建不必要的属性。
36. 寄生式组合继承的实现?
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log("My name is " + this.name + ".");
};
function Student(name, grade) {
Person.call(this, name);
this.grade = grade;
}
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.sayMyGrade = function() {
console.log("My grade is " + this.grade + ".");
};
37. Javascript 的作用域链?
作用域链的作用是保证对执行环境有权访问的所有变量和函数的有序访问,通过作用域链,我们可以访问到外层环境的变量和
函数。
作用域链的本质上是一个指向变量对象的指针列表。变量对象是一个包含了执行环境中所有变量和函数的对象。作用域链的前端始终都是当前执行上下文的变量对象。全局执行上下文的变量对象(也就是全局对象)始终是作用域链的最后一个对象。
当我们查找一个变量时,如果当前执行环境中没有找到,我们可以沿着作用域链向后查找。
作用域链的创建过程跟执行上下文的建立有关....
38. 谈谈 This 对象的理解。
this 是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际开发中,this 的指向可以通过四种调用模式来判断。
第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。
第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。
第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。
第四种是 apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。
其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。
也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。
这四种方式,使用构造器调用模式的优先级最高,然后是 apply 、 call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
39. eval 是做什么的?
它的功能是把对应的字符串解析成 JS 代码并运行。
应该避免使用 eval,不安全,非常耗性能(2次,一次解析成 js 语句,一次执行)。
40. 什么是 DOM 和 BOM?
DOM 指的是文档对象模型,它指的是把文档当做一个对象来对待,这个对象主要定义了处理网页内容的方法和接口。
BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的法和接口。
BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。
window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。
41. 写一个通用的事件侦听器函数。
const EventUtils = {
// 视能力分别使用dom0||dom2||IE方式 来绑定事件
// 添加事件
addEvent: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
// 移除事件
removeEvent: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
},
// 获取事件目标
getTarget: function(event) {
return event.target || event.srcElement;
},
// 获取 event 对象的引用,取到事件的所有信息,确保随时能使用 event
getEvent: function(event) {
return event || window.event;
},
// 阻止事件(主要是事件冒泡,因为 IE 不支持事件捕获)
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
},
// 取消事件的默认行为
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
}
};
42. 事件是什么?IE 与火狐的事件机制有什么区别? 如何阻止冒泡?
(1)事件是用户操作网页时发生的交互动作,比如 click/move, 事件除了用户触发的动作外,还可以是文档加载,窗口滚动和大小调整。事件被封装成一个 event 对象,包含了该事件发生时的所有相关信息( event 的属性)以及可以对事件进行的操作( event 的方法)。
(2)事件处理机制:IE 支持事件冒泡、Firefox 同时支持两种事件模型,也就是:事件冒泡和事件捕获。
(3)event.stopPropagation() 或者 ie 下的方法 event.cancelBubble = true;
43. 三种事件模型是什么?
事件是用户操作网页时发生的交互动作或者网页本身的一些操作,现代浏览器一共有三种事件模型。
第一种事件模型是最早的 DOM0 级模型,这种模型不会传播,所以没有事件流的概念,但是现在有的浏览器支持以冒泡的方式实现,它可以在网页中直接定义监听函数,也可以通过 js 属性来指定监听函数。这种方式是所有浏览器都兼容的。
第二种事件模型是 IE 事件模型,在该事件模型中,一次事件共有两个过程,事件处理阶段,和事件冒泡阶段。
事件处理阶段会首先执行目标元素绑定的监听事件。然后是事件冒泡阶段,冒泡指的是事件从目标元素冒泡到 document,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。这种模型通过 attachEvent 来添加监听函数,可以添加多个监听函数,会按顺序依次执行。
第三种是 DOM2 级事件模型,在该事件模型中,一次事件共有三个过程,第一个过程是事件捕获阶段。
捕获指的是事件从 document 一直向下传播到目标元素,依次检查经过的节点是否绑定了事件监听函数,如果有则执行。
后面两个阶段和 IE 事件模型的两个阶段相同。这种事件模型,事件绑定的函数是 addEventListener,其中第三个参数可以指定事件是否在捕获阶段执行。
44. 事件委托是什么?
事件委托本质上是利用了浏览器事件冒泡的机制。因为事件在冒泡过程中会上传到父节点,并且父节点可以通过事件对象获取到目标节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理多个子元素的事件,这种方式称为事件代理。
使用事件代理我们可以不必要为每一个子元素都绑定一个监听事件,这样减少了内存上的消耗。并且使用事件代理我们还可以实现事件的动态绑定,比如说新增了一个子节点,我们并不需要单独地为它添加一个监听事件,它所发生的事件会交给父元素中的监听函数来处理。
45. ["1", "2", "3"].map(parseInt) 答案是多少?
parseInt() 函数能解析一个字符串,并返回一个整数,需要两个参数 (val, radix),其中 radix 表示要解析的数字的基数。(该值介于 2 ~ 36 之间,并且字符串中的数字不能大于 radix 才能正确返回数字结果值)。
此处 map 传了 3 个参数 (element, index, array),默认第三个参数被忽略掉,因此三次传入的参数分别为 "1-0", "2-1", "3-2"
因为字符串的值不能大于基数,因此后面两次调用均失败,返回 NaN ,第一次基数为 0 ,按十进制解析返回 1。
46. 什么是闭包,为什么要用它?
闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。
闭包有两个常用的用途。
闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,我们可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
函数的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。
其实闭包的本质就是作用域链的一个特殊的应用,只要了解了作用域链的创建过程,就能够理解闭包的实现原理。
47. javascript 代码中的 "use strict"; 是什么意思 ? 使用它区别是什么?
相关知识点:
use strict 是一种 ECMAscript5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行。
设立"严格模式"的目的,主要有以下几个:
- 消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为;
- 消除代码运行的一些不安全之处,保证代码运行的安全;
- 提高编译器效率,增加运行速度;
- 为未来新版本的 Javascript 做好铺垫。
区别:
- 禁止使用 with 语句。
- 禁止 this 关键字指向全局对象。
- 对象不能有重名的属性。
回答:
use strict 指的是严格运行模式,在这种模式对 js 的使用添加了一些限制。比如说禁止 this 指向全局对象,还有禁止使用 with 语句等。
设立严格模式的目的,主要是为了消除代码使用中的一些不安全的使用方式,也是为了消除 js 语法本身的一些不合理的地方,以此来减少一些运行时的怪异的行为。同时使用严格运行模式也能够提高编译的效率,从而提高代码的运行速度。
我认为严格模式代表了 js 一种更合理、更安全、更严谨的发展方向。
48. 如何判断一个对象是否属于某个类?
第一种方式是使用 instanceof 运算符来判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
第二种方式可以通过对象的 constructor 属性来判断,对象的 constructor 属性指向该对象的构造函数,但是这种方式不是很安全,因为 constructor 属性可以被改写。
第三种方式,如果需要判断的是某个内置的引用类型的话,可以使用 Object.prototype.toString()
方法来打印对象的[[Class]]
属性来进行判断。
49. instanceof 的作用?
instanceof 运算符用于判断构造函数的 prototype 属性是否出现在对象的原型链中的任何位置。
实现:
function myInstanceof(left, right) {
let proto = Object.getPrototypeOf(left), // 获取对象的原型
prototype = right.prototype; // 获取构造函数的 prototype 对象
// 判断构造函数的 prototype 对象是否在对象的原型链上
while (true) {
if (!proto) return false;
if (proto === prototype) return true;
proto = Object.getPrototypeOf(proto);
}
}
50. new 操作符具体干了什么呢?如何实现?
(1)首先创建了一个新的空对象
(2)设置原型,将对象的原型设置为函数的 prototype 对象。
(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)
(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。
实现:
function objectFactory() {
let newObject = null,
constructor = Array.prototype.shift.call(arguments),
result = null;
// 参数判断
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 新建一个空对象,对象的原型为构造函数的 prototype 对象
newObject = Object.create(constructor.prototype);
// 将 this 指向新建对象,并执行函数
result = constructor.apply(newObject, arguments);
// 判断返回对象
let flag =
result && (typeof result === "object" || typeof result === "function");
// 判断返回结果
return flag ? result : newObject;
}
// 使用方法
// objectFactory(构造函数, 初始化参数);
作者:CavsZhouyou
转载链接:
https://github.com/CavsZhouyou/Front-End-Interview-Notebook
文源网络,仅供学习之用,如有侵权请联系删除。
我将面试题和答案都整理成了PDF文档,还有一套学习资料,涵盖Java虚拟机、spring框架、Java线程、数据结构、设计模式等等,但不仅限于此。
关注公众号【java圈子】获取资料,还有优质文章每日送达。