JavaScript Ⅰ
- JavaScript 初识
- 运算符
- 变量、值、数据类型、数据类型转换
- 流程控制语句
- 对象
- 数组
- 如何通过js判断是否是数组
- 关于数组是否改变原数组得值 需要注意传参是对象还是基本数据类型还是引用数据类型 详见:[详解](https://blog.csdn.net/ZhengKehang/article/details/81281563)
- 写一个函数 squireArr,其参数是一个数组,作用是把数组中的每一项变为原值的平方。
- 写一个函数 squireArr,其参数是一个数组,返回一个新的数组,新数组中的每一项是原数组对应值的平方,原数组不变。
- 遍历 company 对象,输出里面每一项的值。
- 遍历数组,打印数组里的每一项的平方。
- 以下代码输出什么?
- 以下代码输出什么?
- 使用递归完成 1 到 100 的累加?
- 数组方法
- 正则表达式
- JSON
- DOM
- DOM事件
JavaScript 初识
怎么判断页面是否加载完成?
- Load 事件触发代表页面中的 DOM,CSS,JS,图片已经全部加载完毕。
- DOMContentLoaded 事件触发代表初始的 HTML 被完全加载和解析,不需要等待 CSS,JS,图片加载
说几条写 JavaScript 的基本规范?
-
不要在同一行声明多个变量;
-
请使用
===/!==
来比较 true/false 或者数值; -
使用对象字面量替代 new Array 这种形式;
-
不要使用全局函数;
-
switch 语句必须带有 default 分支;
-
if 语句必须使用大括号;
-
for-in 循环中的变量 应该使用 var 关键字明确限定作用域,从而避免作用域污染。
JavaScript 代码中的 “use strict” 是什么意思?
“use strict” 是一种 ECMAscript 5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行,使 JS 编码更加规范化的模式,消除 Javascript 语法的一些不合理、不严谨之处,减少一些怪异行为。
说说严格模式的限制?
-
变量必须声明后再使用;
-
函数的参数不能有同名属性,否则报错;
-
不能使用 with 语句;
-
禁止 this 指向全局对象。
JavaScript 代码嵌入网页的办法
<script>
元素直接嵌入代码。<script>
标签加载外部脚本- 事件属性
网页元素的事件属性(比如onclick和onmouseover),可以写入 JavaScript 代码。当指定事件发生时,就会调用这些代码。
<button id="myBtn" onclick="console.log(this.id)">点击</button>
上面的事件属性代码只有一个语句。如果有多个语句,使用分号分隔即可。
- URL 协议
- URL 支持javascript:协议,即在 URL 的位置写入代码,使用这个 URL 的时候就会执行 JavaScript 代码。
<a href="javascript:console.log('Hello')">点击</a>
浏览器的地址栏也可以执行javascript:协议。将javascript:console.log('Hello')
放入地址栏,按回车键也会执行这段代码。
如果 JavaScript 代码返回一个字符串,浏览器就会新建一个文档,展示这个字符串的内容,原有文档的内容都会消失。
<a href="javascript: new Date().toLocaleTimeString();">点击</a>
上面代码中,用户点击链接以后,会打开一个新文档,里面有当前时间。
如果返回的不是字符串,那么浏览器不会新建文档,也不会跳转。
<a href="javascript: console.log(new Date().toLocaleTimeString())">点击</a>
上面代码中,用户点击链接后,网页不会跳转,只会在控制台显示当前时间。
js脚本为什么放在最后
https://wangdoc.com/javascript/bom/engine.html
- JavaScript 代码可以修改 DOM 会导致复杂的线程竞赛的问题。
- 所以解析过程中,浏览器发现
<script>
元素,就暂停解析,把网页渲染的控制权转交给 JavaScript 引擎。 - JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页。
- 如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
- 脚本文件都放在网页尾部加载,还有一个好处。因为在 DOM 结构生成之前就调用 DOM 节点,JavaScript 会报错,如果脚本都在网页尾部加载,就不存在这个问题,因为这时 DOM 肯定已经生成了。
如何在DOM加载后执行js
三个基本方法
- 一种解决方法是设定DOMContentLoaded事件的回调函数。
DOMContentLoaded事件只有在 DOM 结构生成之后才会触发。 - 使用
<script>
标签的onload属性。当<script>
标签指定的外部脚本文件下载和解析完成,会触发一个load事件,可以把所需执行的代码,放在这个事件的回调函数里面。 - 放在底部
defer与async
共同点
- 解析过程中,发现带有defer/async属性的
<script>
元素。 - 浏览器继续往下解析 HTML 网页,同时并行下载
<script>
元素加载的外部脚本。 - 浏览器完成解析 HTML 网页,此时再回过头执行已经下载完成的脚本。
- -使用defer/async加载的外部脚本不应该使用document.write方法。
不同点
- 对于内置而不是加载外部脚本的script标签,以及动态生成的script标签,defer属性不起作用。
- defer按序执行,如果脚本之间有依赖关系,就使用defer属性
- async谁先下载好执行谁,如果脚本之间没有依赖关系,就使用async属性
优先级
- 如果同时使用async和defer属性,后者不起作用,浏览器行为由async属性决定
运算符
NaN 是什么?有什么特别之处?
- NaN ——not a number :表示为“不是数字的数字”。所以可以理解为:它也是一个数字类型,不过它不是一个有效的数,表示为错误数字!
- 这个特殊的值是因为运算不能执行而导致的,不能执行的原因要么是因为其中的运算对象之一非数字。例如: “abc” / 4,要么是因为运算的结果非数字。例如:除数为零。
- 特别之处:NaN 和 NaN 是不相等的,即 NaN 与自己不相等
如何判断一个值是否等于NaN?
undefined + 1 //NaN
1、利用NaN的特性:不等于自身
function myIsNaN(n) {
return n !== n
}
let a = 1;
let b = 2;
let c = 'bar';
let d = a * b;
let e = a * c;
console.log(myIsNaN(d)) // false
console.log(myIsNaN(e)) // true
2、 使用全局内置isNaN方法加上判断数据类型(不够准确,内部会有转换 ,isNaN(‘str’) = true)
function myIsNaN2(n) {
return isNaN(n) && **typeof n === 'number'**
}
3、使用Number.isNaN() 方法
function myIsNaN2(n) {
return isNaN(n)
}
4、利用ES6的Object.is方法
function myIsNaN3(n) {
return Object.is(n, NaN)
}
== 与 === 有什么区别?
-
=== 是严格意义的相等,转换规则是:当且仅当两个值的类型和值都相同时,它们才是严格相等的。
-
== 是近似相等,涉及到的转换规则很多,总原则:尽可能地将两边的表达式转化为数字后再比较:
这里来解析一道题目 [] == ![] // -> true ,下面是这个表达式为何为 true 的步骤
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
为什么 console.log(0.2+0.1==0.3) 输出 false ?
-
JavaScript 中的 number 类型就是浮点型,JavaScript 中的浮点数采用二进制浮点数表示法,并不能精确的表示类似 0.1 这样的简单的数字,会有舍入误差。
-
由于采用二进制,JavaScript 也不能有限表示 1/10、1/2 等这样的分数。在二进制中,1/10(0.1)被表示为 0.00110011001100110011…… 注意 0011 是无限重复的,这是舍入误差造成的,所以对于 0.1 + 0.2 这样的运算,操作数会先被转成二进制,然后再计算。
以下代码输出的结果是?
(function(){
var a=b=3;
})();
console.log(typeof a!=='undefined');
console.log(typeof b!=='undefined');
答:
(function(){
var a=b=3;//从右向左,b=3,var a=b;
})();
console.log(typeof a!=='undefined');//a则在函数中进行了声明,并把b的值赋值给a ,a作为局部变量。
console.log(typeof b!=='undefined');//b没有声明,而是直接赋值,可作为全局变量,
//结果:
a defined? false
b defined? true
以下代码输出什么?
var d = (a = 3, b = 4)
console.log(d)
输出 4
以下代码输出结果是?为什么?
var a = 1;
var b = 3;
console.log( a+++b );
答:
var a = 1;
var b = 3;
console.log( a+++b ); //-->4 ++的优先级高于+,相当于(a++)+b=1+3=4
var a = 1;
console.log(a+++a) //3
var a = 1;
console.log(++a+a) //4
以下代码输出的结果是?
var a = 1, b = 2, c = 3;
var val = typeof a + b || c > 0
console.log(val)
var d = 5;
var data = d == 5 && console.log("bb")
console.log(data)
var data2 = d = 0 || console.log("haha")
console.log(data2)
var x = !!"Hello" + (!"world", !!"from here!!");
console.log(x)
答:
var a = 1, b = 2, c = 3;
var val = typeof a + b || c > 0 //typeof 优先级最高,其次是 +、>、||,相当于((typeof a)+b)||(c>0),或 前面部分不为 0 直接返回前面部分结果。
console.log(val) //-->number2
var d = 5;
var data = d == 5 && console.log("bb") //-->bb == 优先级最高,d==5 为 true,所以 && 直接返回后面的值,由于调用了 console.log("bb") ,所以输出 bb。
console.log(data) //-->undefined 由于 console.log() 没有返回值,因此 data=undefined,所以 console.log(data) 的输出结果为 undefined。
var data2 = d = 0 || console.log("haha") //-->haha
console.log(data2) //-->undefined 上题同理
var x = !!"Hello" + (!"world", !!"from here!!");
console.log(x) //-->2 ! 取反,!! 再取反,空字符串为 false,非空则为 true,var x = true+(false+true),true 为 1,1+1=2。
变量、值、数据类型、数据类型转换
JavaScript 定义了几种数据类型?哪些是原始类型?哪些是复杂类型?null 是对象吗?
原始类型:
1. 数值(Number)
2. 字符串(String)
3. 布尔值(Boolean)
4. undefined
5. null
6. Symbol
复杂类型:
7. Object
虽然 typeof null 返回 “object” ,但是 null 不是对象,它是“原始类型”之一
对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
对象类型和原始类型的不同之处:
-
原始类型存储在栈内存,存储的是值;
-
对象类型存储在堆内存,存储的是地址(指针)。当我们把对象赋值给另外一个变量的时候,复制的是地址,指向同一块内存空间,当其中一个对象改变时,另一个对象也会变化。
当函数参数是对象时:
-
首先,函数传参是传递对象指针的副本;
-
到函数内部修改参数的属性这步,我相信大家都知道,当前 p1 的值也被修改了;
-
但是当我们重新为 person 分配了一个对象时就出现了分歧;
-
person拥有了一个新的地址(指针),也就和p1没有任何关系了,导致了最终两个变量的值是不相同的。
function test(person) {
person.age = 2
person = {
name: "aman",
age: 3
}
return person
}
const p1 = {
name: "oli",
age: 5
}
const p2 = test(p1)
console.log(p1) //-->{name: "oli", age: 2}
console.log(p2) // -->{name: "aman", age: 3}
怎样判断“值”属于哪种类型?typeof 是否能正确判断类型?instanceof 呢?instanceof 有什么作用?内部逻辑是如何实现的?
JS 有三种方法可以确定一个值属于哪种数据类型:
-
typeof 运算符;
-
instanceof 运算符;
-
Object.prototype.toString.call() 方法。
typeof 能够正确的判断基本数据类型,除了 null,typeof null 输出的是对象。
但是从对象来说,typeof 不能正确的判断其类型, typeof 一个函数可以输出 “function”,而除此之外,输出的全是 object,这种情况下,我们无法准确的知道对象的类型。
instanceof 可以准确的判断复杂数据类型,但是不能正确判断基本数据类型。
instanceof 是通过原型链判断的。A instanceof B,在 A 的原型链中层层查找,是否有原型等于 B.prototype 。如果一直找到 A 的原型链的顶端(即 Object.prototype.__proto__
),仍然不等于 B.prototype ,那么返回 false,否则返回 true。
null,undefined 的区别?
详见:https://wangdoc.com/javascript/types/null-undefined-boolean.html
undefined 值判定为未处理、未定义或不存在。
表示:目前未定义,所以此处暂时没有任何值,但之后可以去放东西。
当一个变量没有赋值时,只能是 undefined,不会是 null。
null 值为经过处理之后的无值状态;
表示:此处的值就是“无”的状态。
变量提升
- 当执行 JS 代码时,会生成执行环境,只有两种执行环境
不在函数中的代码在全局执行环境中,
函数中的代码会产生函数执行环境,。 - 在生成执行环境时,会有两个阶段。
第一个阶段是创建的阶段,JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined, - 所以在第二个阶段,也就是代码执行阶段,我们可以直接提前使用
- 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
以下代码的输出?为什么?
console.log(a);
var a = 1;
console.log(b);
答:
console.log(a); // -->undefined 因为下面的 var a=1 相当于 var a,a=1。即声明了变量 a 也给 a 赋值了,
因为变量提升的原因,所以在执行第一句时知道了 a 已经被声明但是却没有被赋值。所以显示为 undefined。
var a = 1;
console.log(b); //-->报错,b is not defined。 代码中没有声明变量 b 也没有对其赋值,所以显示为
b is not defined
以下代码输出什么?
var a = typeof 3 + 4
console.log(a)
输出 number4
以下代码输出什么?
var a = typeof typeof 4+4
console.log(a)
输出 string4
流程控制语句
break,continue,return区别
详见:https://www.jianshu.com/p/7712c2f23efd
“break 语句”用于强制退出循环体,执行循环后面的语句;
“continue 语句”是终止本次循环的执行并开始下一次循环的执行。
switch case 语句中的 break 有什么作用?
break 语句通常用在循环语句和开关语句中。当 break 用于开关语句 switch 中时,可使程序跳出 switch 而执行 switch 以后
的语句;如果没有 break 语句,则会从满足条件的地方(即与 switch(表达式)括号中表达式匹配的 case)开始执行,直到 switch 结构结束。
当 break 语句用于 do-while、for、while 循环语句中时,可使程序终止循环。而执行循环后面的语句,通常 break 语句总是与 if 语句联在一起。即满足条件时便跳出循环。
注意:
break 语句对 if-else 的条件语句不起作用;
在多层循环中,一个 break 语句只向外跳一层。
for of、for in 和 forEach、map 的区别?
详见:https://www.jianshu.com/p/e8e04e33fa4d
http://blog.sina.com.cn/s/blog_c112a2980102xqg9.html
- for…of适用遍历数/数组对象/字符串/map/set等拥有迭代器对象的集合.但是不能遍历对象,因为没有迭代器对象.与forEach()不同的是,它可以正确响应break、continue和return语句
- for-in 循环:遍历对象自身的和继承的可枚举的属性, 不能直接获取属性值。可以中断循环。
- forEach: 只能遍历数组,不能中断,没有返回值(或认为返回值是 undefined)。
- map: 只能遍历数组,不能中断,它会返回一个新的数组,所以在callback需要有return值,如果没有,会返回undefined。
for in 与 for循环 性能问题
-
在for in中,for-in需要分析出array的每个属性,这个操作性能开销很大。用在 key 已知的数组上是非常不划算的。所以尽量不要用for-in,除非你不清楚要处理哪些属性,例如 JSON对象这样的情况
-
在for中,循环每进行一次,就要检查一下数组长度。读取属性(数组长度)要比读局部变量慢,尤其是当 array 里存放的都是 DOM 元素,因为每次读取都会扫描一遍页面上的选择器相关元素,速度会大大降低
写出如下知识点的代码范例:
① if-else 的用法;
② switch-case 的用法;
③ while 的用法;
④ do-while 的用法;
⑤ for 遍历数组的用法;
⑥ for-in 遍历对象的用法;
⑦ break 和 continue 的用法。
见:答案
以下代码输出什么?
var a = 2
if(a = 1) { //a=1,if(a)
console.log('a等于1')
}else{
console.log('a不等于1')
}
输出:a等于1
对象
创建对象的方法:
- 使用 Object 构造函数创建
- 使用 Object 构造函数创建
- 工厂模式
缺点:批量创建出属性和方法一致的对象,创建出的对象不能确定其类型,每次创建一个对象都需要创建一次方法,方法复用度不高。 - 构造函数模式(模仿类)
缺点: 函数复用度不高 - 组合模式(构造函数+原型继承 )
利用了构造函数来为对象添加属性,弥补了原型模式“属性共用”的问题;而使用原型模式来为对象添加方法,弥补了构造函数方法复用率不高的问题。 - 使用 ES6 Class(类)
ES6 的 class 语法是并不是向 JavaScript 中引入了一种新的“ 类” 机制。 class 基本上只是现有 [[Prototype]] 机制的一种语法糖。 - Object.create()
原理: 让newObj的原型对象是oldObj (以前是这个函数的prototype 那么就让一个构造函数的prototype = oldObj 再new 这个函数即可)
附:Object.create()详解
if (typeof Object.create !== 'function') {
Object.create = function (obj) {
function F() {}
F.prototype = obj;
return new F();
};
}
上面代码表明,Object.create方法的实质是新建一个空的构造函数F,然后让F.prototype属性指向参数对象obj,最后返回一个F的实例,从而实现让该实例继承obj的属性。
new 的原理是什么?通过 new 的方式创建对象和通过字面量创建有什么区别?
new 的原理(步骤):
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的prototype属性。
- 将这个空对象赋值给函数内部的this关键字。并 开始执行构造函数内部的代码。并执行了构造函数中的方法;
- 如果函数没有返回其他对象,那么 this 指向这个新对象,否则 this 指向构造函数中返回的对象。
区别:
-
字面量创建对象,不会调用 Object 构造函数, 简洁且性能更好;
-
new Object() 方式创建对象本质上是方法调用,涉及到在 proto 链中遍历该方法,当找到该方法后,又会生产方法调用必须的 堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。
new实现
注意事项:
如果构造函数内部有return语句,且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象。
new命令内部流程
function _new(constructor, params) {
let args = [].slice.call(arguments, 1);
let obj = Object.create(constructor.prototype);
let ctx = constructor.apply(obj, args);
return (typeof ctx === 'object' && ctx != null) ? ctx : obj
}
继承
构造函数的继承
第一步:子类继承父类的实例(在子类的构造函数中,调用父类的构造函数。)
//在子类的构造函数中,调用父类的构造函数。
function Sub(value) {
Super.call(this);
this.prop = value;
}
第二步,子类继承父类的原型(让子类的原型指向父类的原型)
//让子类的原型指向父类的原型
Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = '...';
或者:
Sub.prototype = new Super();
Class 的继承
通过 extends 关键字实现继承
ES6 Class 与 普通构造函数的不同点主要有 4 个:
创建时
- constructor 方法
constructor方法是类的默认方法,通过 new 命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有
显式定义,一个空的constructor方法会被默认添加。 - 类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这一点与普通构造函数的行为不一致。
- Class不存在变量提升(hoist),这一点与ES5完全不同。
继承时
- 子类的继承
Class 之间可以通过 extends 关键字实现继承,这比普通构造函数通过修改原型链实现继承,要清晰和方便很多。 - super()在子类constructor构造方法中是为了获取this上下文环境,所以如果在constructor中使用到this,必须在使用this之前调用super(),反之不在constructor中使用this则不必调用super()
super(name, age) 相当于 Foo.prototype.constructor.call(this, name, age) //调用的是constructor方法而非属性
虽然
Foo.prototype.constructor === Foo //true
但是不可以用Foo.call(this,name,age)
因为
类的构造函数,不使用new是没法调用的
– super作为函数调用时,代表父类的构造函数。
– super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。
闭包
定义
- 可以把闭包简单理解成“定义在一个函数内部的函数
- 闭包可以看作是函数内部作用域的一个接口
- 闭包是指有权访问另一个函数作用域中变量的函数,
- 创建闭包的最常见的方式就是在一个函数内创建另一个函数,通过另一个函数访问这个函数的局部变量,利用闭包可以突破作用链域
闭包的特性:
- 函数内再嵌套函数
- 内部函数可以引用外层的参数和变量
- 参数和变量不会被垃圾回收机制回收
闭包优缺点
优点
- 可以避免全局变量的污染,
- 允许函数私有成员的存在
- 允许变量长驻内存
缺点
- 闭包会常驻内存,会增大内存使用量,如:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题。
- 闭包在早期IE中会造成内存泄漏:解决方法是,在退出函数之前,将不使用的局部变量全部删除。
关于闭包内存泄漏原理
https://www.cnblogs.com/blowfish/p/3323357.html
https://segmentfault.com/a/1190000007315908
闭包并不会引起内存泄漏,只是由于IE9之前的版本对JScript对象(标记清除)和COM对象(计数)使用不同的垃圾回收机制,从
而导致内存无法进行回收,这是IE的问题,所以闭包和内存泄漏没半毛钱关系。闭包导致内存泄漏的一个原因就是这个算法的一
个缺陷。循环引用会导致没法回收,这个循环引用只限定于有宿主对象参与的循环引用,而js对象之间即使形成循环引用,也不
会产生内存泄漏,因为对js对象的回收算法不是计数的方式。
闭包用处
-
闭包最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在。
-
闭包的另一个用处,是封装对象的私有属性和私有方法
原型与原型链
- 每个函数都有一个prototype属性即对象属性,指向一个对象。即该函数的实例对象的原型对象。
- 每个对象都有__proto__和constructor属性,函数也是对象。__proto__由对象指向该对象的原型对象,
即该对象的构造函数的原型属性所指向的对象()。constructor是由对象指向该对象的构造函数
- 所有函数和对象最终都是由Function构造函数得来,所以constructor属性的终点就是Function这个函数
- 所有对象的原型最终都可以上溯到Object.prototype,Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。
因此,原型链的尽头就是null。
- _proto__属性的作用就是当访问一个对象的属性时,如果该对象内部不存在这个属性,那么就会去它的__proto__属性所指向
的那个对象(父对象)里找,一直找,直到__proto__属性的终点null,再往上找就相当于在null上取值会报错。
-通过__proto__属性将对象连接起来的这条链路即我们所谓的原型链。
this指向
- 在 es5 中,this 永远指向最后调用它的那个对象。
- 严格模式下不可以指向全局对象 因为屎undefined 会报错
- 箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined。
《javascript语言精髓》中大概概括了4种调用方式:
-
方法调用模式
-
函数调用模式
-
构造器调用模式
-
apply/call调用模式
执行上下文
-
是当前 JavaScript 代码被解析和执行时所在环境
-
在生成执行上下文时,会有两个阶段。
- 创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined
- 在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升
- 代码执行阶段,我们可以直接提前使用。
- 创建的阶段(具体步骤是创建 VO),JS 解释器会找出需要提升的变量和函数,并且给他们提前在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明并且赋值为 undefined
-
当执行 JS 代码时,会产生三种执行上下文
- 全局执行上下文
- 函数执行上下文
- eval 执行上下文
- 浏览器的JS执行引擎总是访问栈顶的执行上下文。
- 全局上下文只有唯一的一个,它在浏览器关闭时出栈。
-
每个执行上下文中都有三个重要的属性
- 变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
- 作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
- this
内存泄漏
定义:程序中己动态分配的堆内存由于某种原因程序未释放或无法释放引发的各种问题。
js中可能出现的内存泄漏导致的现象
变慢,崩溃,延迟大等,
造成的原因
- 全局变量
- dom清空时,还存在引用
- ie中使用闭包
- 定时器未清除
避免策略
- 减少不必要的全局变量,或者生命周期较长的对象,及时对无用的数据进行垃圾回收;
- 注意程序逻辑,避免“死循环”之类的 ;
- 避免创建过多的对象 原则:不用了的东西要及时归还。
- 减少层级过多的引用
垃圾回收机制
Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存。
原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。
通常情况下有两种实现方式:
标记清除
js 中最常用的垃圾回收方式就是标记清除。
当变量进入环境时,例如,在函数中声明一个变量,就将这个变量标记为“进入环境”。从逻辑上讲,永远不能释放进入环境的变量所占用的内存,因为只要执行流进入相应的环境,就可能会用到它们。而当变量离开环境时,则将其标记为“离开环境”。
引用计数
引用计数的含义是跟踪记录每个值被引用的次数。
当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾回收器下次再运行时,它就会释放那些引用次数为 0 的值所占用的内存。
如下代码输出多少?如果想输出 3,那如何改造代码?
var fnArr = [];
for(var i=0; i<10; i++) {
fnArr[i] = function() {
return i
};
}
console.log(fnArr[3]())
答:输出10
//方法一
var fnArr = []
for(var i=0; i<10; i++) {
fnArr[i] = (function(j) {
return function() {
return j
}
})(i)
}
console.log(fnArr[3]()) //-->3
//方法二
var fnArr = []
for(var i=0; i<10; i++) {
(function(i) {
fnArr[i] = function() {
return i
}
})(i)
}
console.log(fnArr[3]()) //-->3
//方法三
var fnArr = []
for(let i=0; i<10; i++) {
fnArr[i] = function() {
return i
}
}
console.log(fnArr[3]()) //-->3
封装一个 Car 对象。
var Car = (function() {
var speed = 0;
//补充
return {
setSpeed: setSpeed,
getSpeed: getSpeed,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.setSpeed(30)
Car.getSpeed() //-->30
Car.speedUp()
Car.getSpeed() //-->31
Car.speedDown()
Car.getSpeed() //-->30
答:
var Car = (function() {
var speed = 0;
function set(s) {
speed = s
}
function get() {
return speed
}
function speedUp() {
speed++
}
function speedDown() {
speed--
}
return {
setSpeed: setSpeed,
get: get,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.set(30)
Car.get() //-->30
Car.speedUp()
Car.get() //-->31
Car.speedDown()
Car.get() //-->30
如下代码输出多少?如何连续输出 0, 1, 2, 3, 4?
for(var i=0; i<5; i++) {
setTimeout(function() {
console.log("delayer:" + i)
}, 0)
}
输出 5 次 delayer:5。
//方法一
for(var i=0; i<5; i++) {
(function(j) {
setTimeout(function() {
console.log("delayer:" + j)
}, 0)
})(i)
}
//方法二
for(var i=0; i<5; i++) {
setTimeout((function(j) {
return function() {
console.log("delayer:" + j)
}
}(i)), 0)
}
如下代码输出多少?
function makeCounter() {
var count = 0
return function() {
return count++
};
}
var counter = makeCounter()
var counter2 = makeCounter();
console.log(counter()) //-->0
console.log(counter()) //-->1
console.log(counter2()) //-->?
console.log(counter2()) //-->?
答:
console.log(counter2()) //-->0
console.log(counter2()) //-->1
写一个函数,返回参数的平方和?
function sumOfSquares() {
//补全
}
var result = sumOfSquares(2, 3, 4)
var result2 = sumOfSquares(1, 3)
console.log(result) //-->29
console.log(result2) //-->10
答:
function sumOfSquares() {
var sum = 0;
for(var i=0; i<arguments.length; i++) {
sum = sum + arguments[i] * arguments[i];
}
return sum;
}
var result = sumOfSquares(2, 3, 4)
var result2 = sumOfSquares(1, 3)
console.log(result) //-->29
console.log(result2) //-->10
如下代码的输出?为什么?
sayName("world");
sayAge(10);
function sayName(name) {
console.log("hello", name);
}
var sayAge = function(age) {
console.log(age);
};
答:
//-->hello world
//-->sayAge is not a function。error,因为下面这个是函数表达式,而函数表达式不会声明提前,所以
sayAge(10) 在前面调用是无效的。
如下代码的输出?为什么?
var x = 10;
bar()
function bar() {
var x = 30;
function foo() {
console.log(x)
}
foo();
}
答:
//-->30
function foo() {
console.log(x)
} 里面没有 x 的赋值,向上找 var x=30,所以是 30。
如下代码的输出?为什么?
var x = 10
bar()
function foo() {
console.log(x)
}
function bar() {
var x = 30
foo()
}
答:
//-->10
function foo() {
console.log(x)
} 里面没有 x 的赋值,向上找,全局变量里有赋值,所以是 10。
如下代码的输出?为什么?
var a = 1
function fn1() {
function fn3() {
function fn2() {
console.log(a)
}
fn2()
var a = 4
}
var a = 2
return fn3
}
var fn = fn1()
fn() //-->?
答:
//-->undefined
function fn2() {
console.log(a)
} 里面没有,向上找,fn3 里虽然有 var a=4,对自身 fn3 没有影响,但对内嵌函数有影响。
声明的最终目的是为了提前使用,即在定义之前使用,如果不需要提前使用就没有单独声
明的必要,变量是如此,函数也是如此,所以声明不会分配存储空间,只有定义时才会分
配存储空间。函数是单向线,从上向下解析的。
如下代码的输出?为什么?
一:
var a = 1
function fn1() {
function fn2() {
console.log(a)
}
function fn3() {
var a = 4
fn2()
}
var a = 2
return fn3
}
var fn = fn1()
fn() //-->?
答:
//-->2
function fn2() {
console.log(a)
} 里没有赋值,向上找,fn1 里有 var a=2,所以是 2。
二:
var a = 1
function fn1() {
function fn3() {
var a = 4
fn2()
}
var a = 2
return fn3
}
function fn2() {
console.log(a)
}
var fn = fn1()
fn() //-->?
答:
//-->1
function fn2() {
console.log(a)
} 里没有赋值,向上找,全局变量里 var a=1,所以是 1。
三
var a = 1
function fn1() {
function fn2() {
console.log(a)
}
function fn3() {
var a = 4
fn2()
};
fn3()
var a = 2
}
fn1()
//-->?undefined
四:
var a = 1
function fn1() {
function fn2() {
console.log(a)
}
function fn3() {
var a = 4
fn2()
};
var a = 2;
fn3()
}
fn1()
//-->?2
如下代码的输出?为什么?
附录:
内存空间详细图解
执行上下文详解
变量对象详解
匿名函数详解
匿名函数与闭包
var a = 1
var c = {name: "oli", age: 2}
function f1(n) {
++n
}
function f2(obj) {
++obj.age
}
f1(a)
f2(c)
f1(c.age)
console.log(a)
console.log(c)
答:在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量( 即 arguments 对象中的一个元素 )。
在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此该局部变量的变化会反映到函数的外部:
//-->1,全局变量
[object Object] {
age: 3,
name: "oli"
} //全局对象,向上找是 var c = {name: "oli", age: 2},
//而 f2(c) ,函数执行 ++obj.age,所以 age: 3。
数组
如何通过js判断是否是数组
关于数组是否改变原数组得值 需要注意传参是对象还是基本数据类型还是引用数据类型 详见:详解
写一个函数 squireArr,其参数是一个数组,作用是把数组中的每一项变为原值的平方。
var arr = [3, 4, 6]
function squireArr( arr ){
//补全
}
squireArr(arr)
console.log(arr) //-->[9, 16, 36]
答案:
function squireArr(arr) {
for(var i=0; i<arr.length; i++) {
arr[i] = arr[i] * arr[i];
}
}
写一个函数 squireArr,其参数是一个数组,返回一个新的数组,新数组中的每一项是原数组对应值的平方,原数组不变。
var arr = [3, 4, 6]
function squireArr( arr ){
//补全
}
var arr2 = squireArr(arr)
console.log(arr) //-->[3, 4, 6]
console.log(arr2) //-->[9, 16, 36]
答案:
方法一:map
function squireArr(arr) {
let newArr = arr.map(i=>{ //map可以返回一个新的数组
return i*i
});
return newArr
}
方法二:
function squireArr(arr) {
var newArr = [];
for(var i=0; i< arr.length; i++) {
newArr[i] = arr[i] * arr[i];
}
return newArr;
}
遍历 company 对象,输出里面每一项的值。
var company = {
name: 'qdywxs',
age: 3,
sex: '男'
}
答案:
var company = {
name: 'qdywxs',
age: 3,
sex: '男'
};
for (const key in company){
console.log(company[key])
}
遍历数组,打印数组里的每一项的平方。
var arr = [3,4,5]
答:
var arr = [3, 4, 5]
for(var i=0; i<3; i++) {
console.log(arr[i] * arr[i])
}
//-->9 16 25
以下代码输出什么?
var name = "sex"
var company = {
name: "qdywxs",
age: 3,
sex: "男"
}
console.log(company[name])
输出 男
以下代码输出什么?
var name = 'sex'
var company = {
name: 'Oli',
age: 3,
sex: '男'
}
console.log(company.name)
输出 qdywxs
使用递归完成 1 到 100 的累加?
答:
function sum(num) {
if(num == 1) {
return 1;
}
return num + sum(num - 1);
}
console.log(sum(100))
数组方法
数组的哪些 API 会改变原数组?
修改原数组的 API 有:
- sort
- push
- pop
- unshift
- shift
- splice
- reverse
写一个函数,操作数组,返回一个新数组,新数组中只包含正数。
function filterPositive(arr) {
//补全
}
var arr = [3, -1, 2, true]
filterPositive(arr)
console.log(filterPositive(arr)) //-->[3, 2]
function filterPositive(arr) {
var newarr = [];
for (var i = 0; i < arr.length; i++) {
if ((typeof arr[i] != 'boolean') && (arr[i] > 0)) { //留意括号的使用
newarr.push(arr[i])
}
}
;return newarr
}
var arr = [3, -1, 2, true]
filterPositive(arr)
console.log(filterPositive(arr))
//-->[3, 2]
补全代码,实现数组按姓名、年纪、任意字段排序。
var users = [
{name: "John", age: 20, company: "Baidu"},
{name: "Pete", age: 18, company: "Alibaba"},
{name: "Ann", age: 19, company: "Tecent"}
]
users.sort(byField("age"))
users.sort(byField("company"))
答:
var users = [
{name: "John", age: 20, company: "Baidu"},
{name: "Pete", age: 18, company: "Alibaba"},
{name: "Ann", age: 19, company: "Tecent"}
]
function byField(keyname){
return function(user1,user2){
return ueser[keyname] - user2[keyname]
}
}
users.sort(byField("age"))
users.sort(byField("company"))
用 splice 函数分别实现 push、pop、shift、unshift 方法。
如:
function push(arr, value) {
arr.splice(arr.length, 0, value)
return arr.length
}
var arr = [3, 4, 5]
arr.push(10) // arr 变成[3, 4, 5, 10],返回 4。
答:
var a = [3, 4, 5, 6];
function sumPush(arr, x) {
arr.splice(arr.length, 0, x);
return arr;
}
sumPush(a, 7);
console.log(a); //-->[3, 4, 5, 6, 7]
function sumPop(arr) {
arr.splice(arr.length - 1, 1);
return arr;
}
sumPop(a);
console.log(a); //-->[3, 4, 5, 6]
function sumShift(arr) {
arr.splice(0, 1);
return arr;
}
sumShift(a);
console.log(a); //-->[4, 5, 6]
function sumUnshift(arr,x) {
arr.splice(0, 0, x);
return arr;
}
sumUnshift(a, 3);
console.log(a); //-->[3, 4, 5, 6]
如何消除一个数组里面重复的元素?
答:
方法一:
var arr1 = ["1", "1", "3", "5", "2", "24", "4", "4", "a", "a", "b"];
rem(arr){
for(var i=0;i<arr.length;i++){
for(var j=0;j<arr.length;j++){
if(arr[j] === arr[i]){arr.splice(j,1)}
}
}
}
rem(arr1)
方法二:
function qc(arr1) {
let arr = [];
for(let i=0; i<arr1.length; i++) {
if(arr.indexOf(arr1[i]) == -1) {
arr.push(arr1[i])
}
}
return arr;
}
arr1 = ["1", "1", "3", "5", "2", "24", "4", "4", "a", "a", "b"];
console.log(qc(arr1)); //-->["1", "3", "5", "2", "24", "4", "a", "b"]
判断一个变量是否是数组,有哪些办法?
- 在 ECMAScript5 标准中 Array 类增加了一个静态方法 isArray,我们可以直接用 Array.isArray 来判断变量是否是数组。
Array.isArray([1, 2, 3]) //-->true
- 但是某些比较老的浏览器,比如 IE8 及以下,没有实现 Array 的 isArray 方法,那么就需要换一种方式来判断:
Object.prototype.toString.call([1, 2, 3]) //-->"[object Array]"
那么我们定义一个函数来实现数组判断:
function isArray (value) {
if(Object.prototype.toString.call(value) === "[object Array]") {
return true
}
return false
}
- instanceof
[123] instanceof Array //true
[“1”, “2”, “3”].map(parseInt) 答案是多少?
答案是 [1, NaN, NaN] 因为 parseInt 需要两个参数(val, radix),其中 radix 表示解析时用的基数。
map 传了 3 个(element, index, array),对应的 radix 不合法导致解析失败
附注:
parseInt(string, radix)
parseInt() 函数可解析一个字符串,并返回一个整数。
- 语法默认传两个参数 可选。radix表示要解析的数字的基数。该值介于 2 ~ 36 之间。如果省略该参数或其值为 0,则数字将以 10 为基础来解析。如果它以 “0x” 或 “0X” 开头,将以 16 为基数。
如果该参数小于 2 或者大于 36,则 parseInt() 将返回 NaN。
map()
详见:https://www.cnblogs.com/zhaoxinmei-123/p/8927259.html
正则表达式
写一个函数 isValidUsername(str),判断用户输入的是不是合法的用户名(长度 6-20 个字符,只能包括字母、数字、下划线)?
function isValidUsername(str) {
var reg = /^\w{6,20}$/;
return reg.test(str);
}
var str = "oli_aman666";
isValidUsername(str); //-->true
写一个函数 isPhoneNum(str),判断用户输入的是不是手机号?
function isPhoneNum(str) {
var reg = /^1[3578]\d{9}$/g;
return reg.test(str);
}
var str = "13123456789";
isPhoneNum(str); //-->true
写一个函数 isEmail(str),判断用户输入的是不是邮箱?
function isEmail(str) {
var reg = /^[\w|-]+@\w+.com$/;
return reg.test(str);
}
var str = "oli125@qq.com";
isEmail(str); //-->true
写一个函数 trim(str),去除字符串两边的空白字符?
function trim(str) {
return str.replace(/^\s+|\s+$/g,"");
}
var str = " oli and aman ";
trim(str); //-->"oli and aman"
\d,\w,\s,[a-zA-Z0-9],\b,.,*,+,?,x{3},^,$ 分别是什么?
\d 表示数字,等价 [0-9];
\w 表示字符、字母、数字、下划线,等价 [a-zA-Z_0-9];
\s 表示空白符,等价 [\t\n\x0B\f\r];
[a-zA-Z0-9] 表示小写字母、大写字母、数字;
\b 表示单词边界;
. 表示除了回车符、换行符之外的所有字符,等价 [^\r\n];
* 表示 0 次或多次,等价 {0,};
+ 表示 1 次或多次,等价 {1,};
? 表示 0 次或 1 次,等价 {0,1};
x{3} 表示 xxx,即 x 出现3次;
^ 表示开头;
^ 在 [] 里是排除的意思:
例如 [^abc],表示是一个除 "a"、"b"、"c" 之外的任意一个字符。
$ 表示结尾。
什么是贪婪模式和非贪婪模式?
String str = "abcaxc";
Patter p = "ab*c";
在贪婪模式(默认)下,正则引擎“尽可能多”地重复匹配字符。如上面使用模式 p 匹配字符串 str,结果就是匹配到:abcaxc(abc)。
非贪婪模式下,正则引擎尽可能少地重复匹配字符。如上面使用模式 p 匹配字符串 str,结果就是匹配到:abc(abc)。
JSON
JSON 格式的数据需要遵循什么规则?
复合类型的值:数组、对象(不能是函数、正则表达式对象、日期对象);
简单类型的值:字符串、数值(必须以十进制表示)、布尔值和 null(不能使用 NaN、Infinity、-Infinity 和 undefined);
字符串必须使用双引号(不能使用单引号);
对象的键名必须放在双引号里面;
数组或对象最后一个成员的后面不能加逗号。
XML 和 JSON 的区别?
-
数据体积方面:JSON 相对于 XML 来讲,数据的体积小,传递的速度更快些;
-
数据交互方面:JSON 与 JavaScript 的交互更加方便,更容易解析处理,更好的数据交互;
-
数据描述方面:JSON 对数据的描述性比 XML 较差;
-
传输速度方面:JSON 的速度要远远快于 XML
eval 是做什么的?
它的功能是把对应的字符串解析成 JS 代码并运行;
应该避免使用 eval,它不安全,且非常耗性能(2 次,一次解析成 JS 语句,一次执行);
由 JSON 字符串转换为 JSON 对象的时候可以用 eval,var obj =eval("("+ str +")")。
DOM
怎么添加、移除、复制、创建、和查找节点?
- 创建新节点:
createDocumentFragment() //创建一个 DOM 片段
createElement() //创建一个具体的元素
createTextNode() //创建一个文本节点
- 添加、移除、替换、插入:
appendChild()
removeChild()
replaceChild()
insertBefore()
- 查找:
getElementsByTagName() //通过标签名称
getElementsByName() //通过元素的 Name 属性的值
getElementById() //通过元素 ID,唯一性。
offsetHeight/Top,clientHeight/ top与 scrollHeight /top
Height
-
offsetWidth/offsetHeight 返回值包含 content + padding + border,如果有滚动条,也不包含滚动条;
-
clientWidth/clientHeight 返回值只包含 content + padding,如果有滚动条,也不包含滚动条;
-
scrollWidth/scrollHeight 返回值包含 content + padding + 溢出内容的尺寸。
Top
- offsetTop : 相对于最近定位父元素的top偏移,top+margin-top
- clientTop: 容器内部相对于容器本身的top偏移,border-top
- scrollTop: Y轴的滚动条没有,或滚到最上时是0;Y轴的滚动条滚到最下时是 scrollHeight-clientHeight。滚动时通常只能
scrollTop,当scrollTop为 0 到 scrollHeight-clientHeight 是正常的滚动距离,否则就是滚动过头了(手机上的阻尼效果)
attribute 和 property 的区别是什么?
见:https://blog.csdn.net/huangfqi/article/details/82350172
-
attribute 是 DOM 元素在文档中作为 html 标签拥有的属性;
input.getAttribute('value')
-
property 就是 DOM 元素在 JS 中作为对象拥有的属性。
input.value
-
对于 html 的标准属性来说,attribute 和 property 是同步且互相映射的,是会自动更新的(input的value属性除外);但是对于自定义的属性来说,他们是不同步的。
DOM事件
addEventListener,attachEvent,on区别
-
同一个 dom 元素上,on 只能绑定一个同类型事件,后者会覆盖前者,不同类型的事件可以绑定多个。
-
addEventListener可以绑定多个事件: 事件执行顺序按照事件绑定的先后顺序执行;
IE8及一下不支持
三个参数
事件处理程序会在所属元素的作用域内运行
type没有on -
attachEventattchEvent绑定多个事件的执行顺序是随机的。
事件处理程序会在全局作用域中运行, 因此this等于window。
type有on
element.attachEvent(type,listener);
事件模型
W3C中定义事件的发生经历三个阶段:捕获阶段(capturing)、目标阶段(targetin)、冒泡阶段(bubbling)
- 冒泡型事件:当你使用事件冒泡时,子级元素先触发,父级元素后触发
- 捕获型事件:当你使用事件捕获时,父级元素先触发,子级元素后触发
- DOM事件流:同时支持两种事件模型:捕获型事件和冒泡型事件
- 阻止冒泡:在W3c中,使用
stopPropagation()
方法;在IE下设置cancelBubble = true
- 阻止捕获:阻止事件的默认行为,例如
click - <a>
后的跳转。在W3c中,使用preventDefault()
方法,在IE下设置window.event.returnValue = false
事件代理(事件委托)
- 通过事件冒泡给父元素添加事件监听,把原本需要绑定的事件委托给父元素,e.target 指向引发触发事件的元素。
- 如果一个节点中的子节点是动态生成的,那么子节点需要注册事件的话应该注册在父节点上
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
- 事件代理的方式相对于直接给目标注册事件来说,有以下优点
节省内存
不需要给子节点注销事件