慕课 快速搞定前端技术一面 第四五六章 笔记总结
JS变量类型和计算
JS中使用typeof
能得到的哪些类型
- 6种,number、undefined、string、boolean、Symbol、function、object
- 准确判断5种 值类型(值类型中不能判断null) ,但是 引用类型 中只能判断函数,其余的 数组、对象、正则、日期 都被识别为object。
- 注意:值类型中null(空指针)也被识别为object!
- 想要区分引用类型可使用instanceof运算符
- 基本(原始)、复杂与全局数据类型
何时使用===
何时使用==
- 注意:不要混淆,只有NaN和自己不相等,null和undefined都和自己是相等的。
- 【需要值为null或者undefined时使用
==
简写,因为null==undefined
】
只有一种情况使用双等号==
,其他时候全部使用三等号===
:
此时第三行代码相当于第四行代码的简写,这是jquery的源码推荐写法。 - 双等号
==
是值相等,而三等号===
是严格相等(值及类型是否完全相等),双等号==
在运算的时候会进行 类型转换,而三等号===
则不会。- 对于string,number等基础类型,
==
和===
是有区别的- 不同类型间比较,
==
会先“转化成同一类型后的值”看“值”是否相等,===
如果类型不同,其结果就是不等==
统一先转换为数字比较(其中Boolean只有两个值:true==1,false==0;null与undefined相等;字符串数字等于数字值,空字符串""
==0
- 同类型比较,直接进行“值”比较,两者结果一样
- 不同类型间比较,
- 对于Array,Object等引用类型,
==
和===
是没有区别的,都是进行“指针地址”比较 - 基础类型与高级类型之间进行比较,
==
和===
是有区别的- 对于
==
,将引用类型转化为基础类型,进行“值”比较 - 因为类型不同,
===
结果为false
- 对于
- 对于string,number等基础类型,
JS中有哪些内置函数
内置函数(数据封装类对象):
Object、Array、Boolean、String、Function、Date、RegExp、Error、Number
JS变量按存储方式分为哪些类型,有何特点
- 按照存储方式区分为:值类型、引用类型
- 值类型(基本类型):名字和值都会储存在栈内存中。
- 引用类型:名字存在 栈内存 中,值存在 堆内存 中,但是 栈内存 会提供一个 引用的地址 指向堆内存中的值(指针)。
- 特点:值类型相当于复制,引用类型则是 指针复制 并不是真正的拷贝,两个变量会共用同一个属性,他们的值的修改是相互干预的。
如何理解JSON
- JSON是一种数据格式,但从JS来看JSON就是一个JS内置对象,他有两个方法,“JavaScript Object Notation” 即 “JavaScript 对象表示法”,它是存储和交换文本信息的语法。
stringify()把对象变成字符串
parse()把字符串变成对象
手写深拷贝
- 注意判断值类型和引用类型
- 注意判断是数组还是对象(判断对象自身属性)
- 递归
- 不使用for…of的原因: 数组元素/对象属性为不可迭代的对象时会报错。
1 | /** |
1 | /** |
原型和原型链
如何准确判断变量是不是数组(typeof不行)
- instanceof判断引用类型(对象、数组、函数)属于哪个构造函数(Object()、Array()、Funtion())
- 注意:数组通过instanceof到Array或Object都返回true
Array.isArray(obj)
class的原型本质
- class实际上是函数(typeof)的语法糖
- 原型规则:
- 引用类型可自由扩展属性
- 引用类型的
__proto__
属性(隐式原型),属性值是一个普通的对象 - 函数有
prototype
属性(显式原型),属性值也是一个普通的对象 - 引用类型(数组、对象、函数)的
__proto__
属性值 都指向它的 构造函数的prototype
属性值 - 当试图得到一个对象的某个属性/方法时,如果这个对象本身没有这个属性,那么会去它的
__proto__
属性(即它的构造函数的prototype
属性)中寻找。
- class与原型规则:
- 每个class(本质是函数)都有显式原型
prototype
- 每个实例都有隐式原型
__proto__
- 实例的
__proto__
指向对应class的prototype
- 子类的显示原型(可理解为一个对象)的隐式原型 指向 父类的显式原型
- 父类的显示原型(可理解为一个对象)的隐式原型 指向 Object(构造函数 )的显式原型
- 每个class(本质是函数)都有显式原型
手写一个简易的jQuery,考虑插件和扩展性
$(selector).get(index)
通过检索匹配jQuery对象得到对应的DOM元素。- $(selector).each(function(index,element))遍历一个jQuery对象,为每个匹配元素执行一个函数。
- 事件绑定on()在选定的元素上绑定一个或多个事件处理函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56class jQuery {
constructor(selector) {
// 使this为DOM元素组成的类数组
const res = document.querySelectorAll(selector);
const length = res.length;
// 遍历类数组对象,放入this中
for (let i = 0; i < length; i++) {
this[i] = res[i];
}
this.length = length;
this.selector = selector;
}
// .get(index)匹配jQuery对象得到对应的DOM元素
get(index) {
return this[index];
}
// each() 方法遍历一个jQuery对象,为每个匹配元素执行一个函数
each(fn) {
for (let i = 0; i < this.length; i++) {
let elem = this[i];
fn(elem);
}
}
// on()在选定的元素上绑定一个或多个事件处理函数。
on(type, fn) {
this.each(elem => {
elem.addEventListener(type, fn, false)
})
}
// 扩展很多 DOM API...
}
// 插件(做一个弹出提示框的插件)
jQuery.prototype.dialog = function (info) {
alert(info);
}
// “造轮子”
class myJQuery extends jQuery {
constructor(selector) {
super(selector)
}
// 扩展自己的方法
addClass(className) {
}
style(data) {
}
}
// let $p=new jQuery("p");
// $p.get(1)
// $p.each((elem) => console.log(elem.nodeName))
// $p.on('click', () => alert('clicked'))
// $p.dialog("abc")
写一个原型链继承的例子
重点:子构造函数.prototype=new 父构造函数;
基础例子:
1-12:创建两个构造函数Animal()、Dog():
13:将Dog的显示类型prototype
更改为Animal()的一个对象:
15:new一个Dog()的对象hashiqi,此时hashiqi不仅有Dog()的属性bark,还有一个继承自Dog.prototype
(即Animal()的一个对象)的eat属性。
【eat属性 就来自原型链继承。】
封装DOM查询的例子
- 功能:获取DOM节点的内容并给节点绑定一个事件。
- 原理:通过扩展原型的方式来:根据id获取一个DOM节点,扩展Elem的显式原型方法html(),调用html()可改变/返回节点的innerHTML。再扩展一个原型方法on(),调用on()可绑定一个事件函数。(调用html()或者on()时都是到对象的隐式原型(即构造函数的显式原型)中找到的,这就是原型链继承)
- 测试方法:先通过不传参的html()测试是否能成功获取该id的DOM节点的innerHTML,可以则传参改变该innerHTML,并使用on()绑定click函数用于点击节点后弹出警示框。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31//根据传入的id获取DOM元素elem
function Elem(id) {
//获取DOM元素并赋予elem
this.elem = document.getElementById(id);
}
// 修改Elem的显式原型,增加html(),使得调用html()可改/返innerHTML
Elem.prototype.html = function (val) {
var elem = this.elem;//绑定elem
//如果有传参就用参数替换掉获取到的DOM元素的innerHTML
if (val) {
elem.innerHTML = val;
// 可不反回this,返回是为了实现链式操作
return this;
} else {
return elem.innerHTML;
}
}
// 同上增on(),使调用on()可绑定事件函数,type是函数名,fn是函数
Elem.prototype.on = function (type, fn) {
var elem = this.elem;
elem.addEventListener(type, fn);
}
var div1 = new Elem("main-navigation");
// console.log(div1.html());说明能够获取innerHTML
div1.html("<p>你好呀</p>");
div1.on("click", function () {
alert("你点击了我!") ;
})
【关于“addEventListener”可参考笔记DOM基础的添加事件句柄。要区别于xx.onclick=function(){函数名(参数);}
】
测试:打开一个网页,找到菜单栏对应的id:
将此id用作参数调用构造函数Elem()创建一个对象div1,可打印出菜单栏的innerHTML,说明可以成功获取到div1的innerHTML:
调用div1的显式原型中的html()进行数据的替换,调用div1的显式原型中的on()进行事件函数的绑定,点击div1后弹出警示框:
补充:链式操作
因为我们的html()返回的是this,相当于返回了div1,所以可以链式操作:
1 | div1.html("<p>你好呀</p>").on("click", function () { |
描述new一个对象的过程
【考察 构造函数】
- 创建一个新(空)对象f
- 将新对象f的隐式原型(
__proto__
)指向构造函数的显式原型(prototype
) - 构造函数Foo()的this指向这个新对象f(这里的this是个空对象)
- 让新对象f去执行Foo()函数代码(即对this进行赋值)
- 如构造函数无返回值或返回一个非对象值,则返回新对象f;如返回值是新对象f则直接返回该对象。(屏蔽第5行代码也是一样的效果,默认返回,此时this已经不是空对象了)
作用域和闭包
对变量提升的理解
【变量定义、函数声明(注意和函数表达式的区别)】
- 首先要明确,变量是声明被提升,使用函数声明方式声明的函数在ES5中是整个函数被提升,在ES6中只提升函数的声明。使用函数表达式声明的函数不存在函数提升,和ES6一样只提升函数的声明(具体见例子),函数内容(变量初始化的值)是不会被提升的,他们将被留在原来的位置。
- 这导致了JavaScript 函数能够在声明之前被调用,不过会undefined,还是建议规范书写代码。
- ES6之前只有全局作用域和函数作用域,这两个作用域都存在变量提升。
var
命令声明的变量,不管在什么位置,变量声明都会被提升到当前作用域的头部。(变量的提升只会对var
命令声明的变量有效,其他不是用var命令声明的变量,不会发生变量的提升。)- 注意:变量的提升只存在全局/函数作用域中,**
if(){}
或for(){}
都不是函数作用域,他们里面的变量会提升到包裹他们的全局/函数作用域的顶部**。 - ES6中取代
var
的let
命令的作用域限定在块级,使用let声明的变量不存在变量提升。 - 函数和变量相比,会被优先提升。这意味着函数会被提升到更靠前的位置。
- 补充:当出现多个同名变量与同名函数时,调用该变量名时的优先级为:变量声明< 函数声明 < 变量赋值(具体参考牛客网20191215第7题)
- 通过函数声明方式创建的函数会被提升,通过表达式方法创建的函数不会被函数提升(他会类似于变量提升):
1
2
3
4
5
6
7
8
9
10
11
12console.log(a) // f a() { console.log(a) }
console.log(b) //undefined
// 函数声明,整个函数提升
function a() {
console.log(a)
}
// 函数表达式,只提升b的声明,即var b=undefined
var b = function(){
console.log(b)
}
this的不同使用场景
- 在普通函数中执行,**
this
指向window
**window.fn1();
和fn1()
效果一样,所以函数fn1()
是由window
对象调用的,根据谁调用函数的this就指向谁的原则,this
指向window
。
- 借助call()/apply()/bind()绑定/改变
this
指向,绑定的this是谁就是谁。- 注意:与call、apply不同,必须要使用**函数表达式创建才能使用
bind()
**,使用bind()
传参的方式也不同,可参考笔记bind()
- 注意:与call、apply不同,必须要使用**函数表达式创建才能使用
- 在对象属性(方法)中 执行,
this
指向对象本身 - 在class/构造函数 里使用
this
,this
指向class实例/构造函数实例本身 - 在箭头函数 里使用
this
,this
是该函数的上级作用域的this
手写bind函数
- 类数组转换为数组的多种方法
- 剩余参数
...
是做聚合的,它会将那些没有对应形参的实参们聚合成一个数组。而扩展运算符...
是做展开的,符号都是...
,但含义不同。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// fn.bind(this,arg1,arg2)()
// 函数的方法,所以写在Fuction原型对象上
Function.prototype.myBind = function (context = window) { // ES6方法,为参数添加默认值
// 传入的参数1 context 设置为this,即现在调用方法的对象
let obj = context
// this为f.myBind(...) 中的 f
obj.fn = this
let args = [...arguments].slice(1);
// 和call、apply不同的是bind返回的是一个新的函数
return function () {
return obj.fn( ...args);
}
}
// 用法:f.myBind(obj,arg1)()
function f(a, b) {
console.log(a + b)
console.log(this.name)
}
let obj = {
name: "李四"
}
f.myBind(obj, 1, 2)() // 3 李四
创建10个a标签,点击时弹出对应的序号
错误的例子
错误原因分析:
- 确实可以得到0-9的10个
<a>
标签。 - 但是**
addEventListener()
中添加的函数时要点击以后才会执行的,当我们10个标签都出现在页面上以后,i值变为10**。 - 而for循环这10下是个很快的过程,等循环结束我们点击a标签,触发click的函数时,i是个自由变量,他要去父级作用域中找i的值,此时i已经循环到了10。
- 所以不管点击哪个标签,弹出的警示框的i都是10,并不是对应的序号。
总结:之所以会错就是因为这里的i是 全局作用域 下的i。
理解方式2:
1.循环是同步的,所以创建标签时得到0-9是没问题的。
2.但 事件绑定是异步的,所以他会等循环全部结束以后才开始执行。
3.可i是全局变量,也就是说每次循环改变的都是同一个i的值,循环结束后i值变为10,此时事件绑定函数才开始执行,那么他每次到父级作用域中取到的其实都是**i=10
,所以不管点击哪个标签弹出的都是10**。
正确示例
- 方法1:【使用闭包的持久性,注意重点不在匿名自执行函数,将i传进函数中作为 函数作用域 的变量】:
i=0
时生成一个函数,i=1
时又生成另外一个函数,总共通过循环创建了10个函数。- 那么第9行代码去获取i的值时就会到当前作用域(函数作用域)中找,自然也不会10个标签都找到同一个i了。
- 方法2:【for中使用let代替var定义i】
1
2
3
4
5
6
7
8
9
10
11
12// let 是块级作用域,所以 let代替var定义i 能起到和闭包相同的效果,每一次循环就产生一个块级作用域
// 注意:let i在for外面没用,那样i的块级作用域太大了(全局作用域)
var a;
for (let i = 0; i < 10; i++) {
a = document.createElement("a");
a.innerHTML = i + "<br/>";
a.addEventListener("click", function (e) {
e.preventDefault();
alert(i);
})
document.body.appendChild(a);
}
如何理解作用域
- 自由变量,即当前作用域还没有定义的变量
- 父级作用域:在函数定义时规定的,不需要管执行顺序。
- 补充:this指向时函数执行时确定的,箭头函数没有自己的this,它则指向自己的父级作用域的this。
- 作用域链:即自由变量的层层查找,函数中找不到 自由变量 的值时一层层向上面的 父级作用域 进行查找的链式流程。
- 闭包的使用场景(也和作用域有关):
- 本质就是在一个函数内部定义另一个函数
- 作用:让一个函数有权访问另一个函数作用域中变量。(即让函数访问自己的父级作用域这个函数中的变量)
实际开发中闭包的应用
隐藏数据,如做一个简单的cache工具:
1 | // 闭包隐藏数据,只提供 API |
判断用户是不是第一次校验:
好处:将存储用户id的 变量_list
封装起来,那么闭包外面就拿不到 变量_list
,减少用户数据泄露的危险。
游戏中的作用:
假设我们在做一个游戏,在写其中关于「还剩几条命」的代码。如果不用闭包,你可以直接用一个全局变量:window.lives = 30 // 还有三十条命
但这样看起来很不妥。万一不小心把这个值改成 -1
了怎么办。所以我们不能让别人「直接访问」这个变量。怎么办呢?
用局部变量。
但是用局部变量别人又访问不到,怎么办呢?
**暴露一个访问器(函数),让别人可以「间接访问」生命值lives
**。
使用闭包后,在其他的 JS 文件中就可以使用 window.奖励一条命()
来涨命,使用 window.死一条命()
来让角色掉一条命。