JavaScript 高级
面向对象编程
什么是面向对象?
面向对象编程 —— Object Oriented Programming,简称 OOP ,是一种编程开发思想。
它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。
在面向对象程序开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。
面向对象与面向过程:
- 面向过程就是亲历亲为,事无巨细,面面俱到,步步紧跟,有条不紊,面向过程是解决问题的一种思维方式,关注点在于解决问题的过程。
- 面向对象就是找一个对象,指挥得结果,解决问题的思维方式,关注点在解决问题的对象上。
- 面向对象将执行者转变成指挥者
- 面向对象不是面向过程的替代,而是面向过程的封装
面向对象编程三大特性
- 封装性:
- 将功能的具体实现,全部封装到对象的内部,外界使用对象时,只需要关注对象提供的方法如何使用,而不需要关心对象对象的内部具体实现,这就是封装。
- 继承性:
- 在js中,继承的概念很简单,一个对象没有的一些属性和方法,另外一个对象有,拿过来用,就实现了继承。
- 注意:在其他语言里面,继承是类与类之间的关系,在js中,是对象与对象之间的关系。
- 多态性:
- 多态是在强类型的语言中才有的。js是弱类型语言,所以JS不支持多态。
prototype 原型
什么叫原型:
- 任何一个函数,都有一个属性,prototype 值是一个对象
- 把函数的 prototype 指向的这个对象就叫做原型。
- 原型内所有的属性和方法都可以被这个构造函数new出来的实例访问。
原型的作用: 存储一些所有实例需要共享的内容,比如方法。
这也就意味着,我们可以把所有对象实例需要共享的属性和方法直接定义在 prototype 对象上。
原型(prototype)的概念
JavaScript中任何一个函数都有一个属性,prototype指向一个对象,这个函数构造的所有实例都可以继承这个对象的所有属性和方法;
原型(prototype)的作用
原型(prototype)的作用是存储所有实例需要共享和继承的属性和方法;
构造函数,实例,原型之间的关系
构造函数:构造函数就是一个函数,配合new可以新建对象。
实例:通过构造函数实例化出来的对象我们把它叫做构造函数的实例。一个构造函数可以有很多实例。
原型:每一个构造函数都有一个属性prototype,这个属性就叫做原型属性。通过构造函数创建出来的实例能够直接使用原型上的属性和方法。
构造函数:其实就一个函数,通常来说,构造函数的首字母要大写。构造函数要通过new来使用。构造函数的作用:实例化对象。
实例(对象): 任何一个对象都是由构造函数new出来。
1 | var obj = {}; // var obj = new Object(); |
原型:任何一个构造函数,都会有一个prototype属性,原型可以理解为构造函数的配偶,或者是实例的父亲。原型就是一个对象。 默认的原型对象只有一个属性:constructor的属性,指向了构造函数。
1 | Person.prototype == 实例.__proto__ |
__proto__
属性
通过构造函数创建的对象,自带一个 __proro__
属性,这个属性指向了构造函数的prototype属性,也就是原型对象。
获取原型对象:
- 通过
构造函数.prototype
可以获取 - 通过
实例.__proto__
可以获取(隐式原型) - 它们指向了同一个
对象构造函数.prototype === 实例.__proto__
【注意:__proto__
是浏览器的一个隐藏(私有)属性,早期的IE浏览器不支持,不要去修改它,如果要修改原型中的内容,使用 构造函数.prototype
去修改】
constructor属性
默认情况下,原型对象中值包含了一个属性:constructor,constructor属性指向了当前的构造函数。
原型链
任何一个对象,都有原型对象,原型对象本身又是一个对象,所以原型对象也有自己的原型对象,这样一环扣一环就形成了一个链式结构,我们把这个链式结构称为:原型链。
总结:Object.prototype
是原型链的尽头,Object.prototype
的原型是 null
。
任何一个函数,都会有 prototype 属性。 任何一个对象,都会有(原型)__proto__
,这个原型又是一个对象,所以原型也会原型(__proto__
),一环扣一环,就形成了一个链式的结构,我们把这个链式结构就叫做原型链。
- 所有的函数的都是
new Fucntion
创建出来的,函数.__proto__ === Function.prototype
- 所有的原型都是
new Object
创建出来的,原型.__proto__ === Object.prototype
1 | Function.__proto__ === Function.prototype |
【Math是一个对象。】
原型链属性查找原则
如果是获取操作
- 会先在自身上查找,如果没有
- 则根据
__proto__
对应的原型去找,如果没有 - 一直找到
Object.prototyp
,如果没有,那就找不到了,返回undefined
。(万物皆对象) - 当我们访问某个对象的属性的时候, 先找自己的属性,如果有,直接返回;如果没有,沿着原型链一直找到
Object.prototype
,如果还没有,就会返回undefined
。
如果是修改操作
- 只会修改对象自身的属性,如果自身没有这个属性,那么就会添加这个属性,并不会修改原型中的属性。
- 如果对象自己有,就覆盖原来的值。如果对象没有,就增加一个值。
Object.prototype成员介绍
hasOwnProperty()
hasOwnProperty()
方法会返回一个布尔值,判断某个属性是否是对象自己的属性。
- 如果是自己的属性,会返回true。
- 如果不是自己的属性或者这个属性不存在,都会返回false。
hasOwnProperty与in的区别
in操作符:如果属性不是自己提供的,是从原型上继承来的,也会返回true;
hasOwnProperty: 该属性必须是自己提供,才返回true,否则返回false。
propertyIsEnumerable()
propertyIsEnumerable()
方法返回一个布尔值,表明指定的属性名是否是当前对象可枚举的自身属性。
- 该属性必须是可枚举,即可以被for..in遍历
- 该属性必须是自身属性。
Object.defineProperty()
了解
1 | //第一个参数:需要给哪个对象添加属性 |
valueOf()/toString()/toLocalString()
valueOf()
会返回对象的原始值,我们不需要手动的调用这个方法,当我们需要使用到原始值的时候,JavaScript会自动的调用他。toString()
方法返回一个表示该对象的字符串。当对象需要转换为一个字符串的时候,toString方法会被调用toLocaleString()
方法返回一个该对象的字符串表示。该方法主要用于被本地化相关对象覆盖。
参考资料:
- http://www.ecma-international.org/ecma-262/5.1/#sec-8.12.8
- http://lzw.me/pages/ecmascript/#100‘
- 当一个对象需要转换成字符串类型的时候,会调用
tostring()
方法,如果需要转换成原始值,会调用valueOf()
方法。- 如果是Date类型,会默认先调用
toString()
方法,如果toString()
类型没有获取到原始值,就会调用valueOf()
方法 - 如果是其他类型,会默认先调用
valueOf()
方法,如果valueOf()
没有获取到原始值,会调用toString()
方法 - 如果
toString()
和valueOf()
方法都没有转换成原始值,就会报错。
- 如果是Date类型,会默认先调用
isPrototypeOf()
isPrototypeOf()
用来判断对象
isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上。
A.isPrototypeOf(B)
判断A是否是B的原型或者是原型的原型。说白了就是判断A是否是B的祖先
A.isPrototypeOf(B)
判断A是否在B的原型链上;A 是一个原型对象
B instanceof A
判断A的prototype是否在B的原型链上;A 是一个构造函数
instanceof
instanceof 判断的是构造函数(实际执行的是构造函数.prototype
)
instanceof 运算符作用和isPrototypeOf类似,用于判断构造函数的prototype属性是否在对象的原型链上。如果是,就返回true,如果不在,就返回false。
语法: 实例对象 instanceof 构造函数
返回值:检测构造函数的prototype属性是否在实例对象的原型链上。
A.isPrototypeOf(B)
判断A是否在B的原型链上;A 是一个原型对象B instanceof A
判断A的prototype是否在B的原型链上;A 是一个构造函数
函数的四种调用模式(this)
函数内部可以使用this,这个this是动态的,函数的调用模式不同,this指向的对象也不同;
函数和方法的区别
对象内部的函数叫方法;
不在一个对象内的函数,叫函数
函数调用模式
this指向window 函数名();
方法调用模式
this指向当前调用函数的对象 对象名.方法名();
构造函数调用模式
this指向 new 构造函数名()
的实例对象;
上下文调用模式(方法借调模式)
call()
call函数调用
fn.call()
所有的函数都可以使用call进行调用;
参数1:指定函数的this,如果不传,则this指向window
其余参数:和函数的参数列表一模一样。
说白了,call方法也可以和()一样,进行函数调用,call方法的第一个参数可以指定函数内部的this指向。
fn.call(thisArg, arg1, arg2, arg2);
thisArg:this指向对象,如果不传,则this指向window
arg1, arg2, arg2:形参
方法借调
obj2.方法名.call(obj1)
obj2借用obj2的方法
伪数组也叫类数组
- 伪数组其实就是一个对象,但是跟数组一样,伪数组也会有length属性,也有0,1,2,3等属性。
- 伪数组并没有数组的方法,不能使用push/pop等方法
- 伪数组可以跟数组一样进行遍历,通过下标操作。
- 常见的伪数组:
arguments
、document.getElementsByTagName
的返回值、jQuery对象
1 | var arrayLike = { |
借调的时候,调用了这个函数;this指向传入的参数;
apply()
apply()
方法
apply()
方法的作用和 call()
方法类似,只有一个区别,就是 apply()
方法接受的是一个包含多个参数的数组。
而call()方法接受的是若干个参数的列表
apply(thisArg, Array)
不传递参数默认指向window;参数格式固定,可以不写,
thisArg:this指向的对象
Array:所有实参组成的数组
call和apply的使用场景:
- 如果参数比较少,使用call会更加简洁
- 如果参数存放在数组中,此时需要使用apply
1 | var arr = [1, 4, 7, 2, 5, 8, 3, 6, 9, 951, 357, 654, 852]; |
沙箱模式
沙箱,沙盒,沙盘:这是一个独立的环境,这里面的任何变量都是这个环境中的变量,与外部无关;
函数自调用就是一个沙箱模式,自调用函数内部不会影响到外部;
如果想让一个变量可以被外部调用,在自调用函数结尾的地方使用 window.变量名 = 变量
1 | (function(window){ |
继承
一个对象可以访问构造函数的原型中的属性和方法,那么如果想要让一个对象增加某些属性和方法,
只需要把这些属性和方法放到原型对象中即可。这样就实现了继承, 称之为原型链继承;
混入式继承(mixin)
把一个对象中的属性和方法拷贝到另一个对象中;
原型链继承
直接给原型添加属性和方法
替换原型,在替换的对象中加入constructor:构造函数名;
混入式继承+原型替换
Object.create
最初是由道格拉斯丶克罗克福德发布的一篇文章提出的,ECMAScript5新增了 Object.create()
方法来规范化了这种继承。
ES5中新增了一个方法 Object.create()
,方法会使用指定的原型对象及其属性去创建一个新的对象。
1 | //参数:proto 一个对象 |
函数进阶
函数声明的三种方式
函数声明 function fn() {}
函数表达式 var fn = function () {}
构造函数 var fn = new Function()
1 | //函数也是对象,可以使用Function构造函数new出来 |
所见即所得函数
new Function可以让一段字符串当成代码来执行。
1 | var str = "var n1 = 1; var n2 = 2; console.log(n1 + n2)"; |
eval函数
eval是一函数,作用可以执行一段js程序;
eval的可以和new Function一样,执行字符串代码
注意:eval函数的功能非常的强大,但是实际使用的情况并不多。
eval形式的代码难以阅读;eval形式的代码无法打断点,因为本质还是还是一个字符串;在浏览器端执行任意的 JavaScript 会带来潜在的安全风险,恶意的JavaScript代码可能会破坏应用;
立即执行函数/自调用函数
自调用函数前面用分号开头,防止上一行代码没有用分号结束,而被浏览器默认连接在一起;
;()()
() [] // `
:`之前的代码必须使用分号结束
函数的原型链
函数也是一个对象:函数是由new Function
创建出来的,因此函数也是一个对象,所有的函数都是new Function
的实例。
函数的完整版原型链
- 所有的函数的都是
new Fucntion
创建出来的,函数.__proto__ === Function.prototype
- 所有的原型都是
new Object
创建出来的,原型.__proto__ === Object.prototype
1 | Function.__proto__ === Function.prototype; |
ECMAScript 族谱
1 | console.log(Object instanceof Function); |
预解析与作用域
预解析详解
预解析:预先解析
js执行代码分为两个过程:
- 预解析过程(变量与函数提升)
- 代码一行一行执行
预解析过程:JavaScript解析器在执行代码前,会把所有变量的声明和函数的声明提升到当前作用域的顶部。例如var a = 11;
其实会分为var a;
和 a = 11
两部分,其中var a;
会被提升。
预解析规则:
- 函数优先,先提升function,后提升var
- 遇到重名的var会被忽略。
- 遇到重名的function会被覆盖。
推荐:不要在一个作用域内重复的声明相同的变量和函数
作用域详解
作用域:变量起作用的区域,作用域决定了一个变量被定义在哪里,以及该如何被查找。
全局变量:在函数外定义的变量就叫全局变量,全局变量在任何地方都能访问到。
局部变量:在函数内定义的变量就叫局部变量,局部变量只有在当前函数内才能访问到。
1 | var num = 11;//全局变量 |
词法作用域
编程语言中,作用域规则分为两种:
- 词法作用域(静态作用域)
- 动态作用域
JavaScript采用的是词法作用域规则,词法作用域也叫静态作用域,变量在函数声明的时候,它的作用域就定下来了,与函数的调用无关。
JavaScript使用的是词法作用域(静态作用域);
函数在声明的时候,作用域就已经确定了;
函数内部的变量查找,与函数在哪里调用无关,只与函数声明的作用域有关;this指向另说;
作用域链
作用域链:只要是函数,就会形成一个作用域,如果这个函数被嵌套在其他函数中,那么外部函数也有自己的作用域,这个一直往上到全局环境,就形成了一个条作用域链。
变量的搜索原则:
- 从当前作用域开始搜索变量,如果存在,那么就直接返回这个变量的值。
- 如果不存在,就会往上一层作用域查询,如果存在,就返回。
- 如果不存在,一直查询到全局作用域,如果存在,就返回。如果不存在说明该变量是不存在的。
- 如果一个变量不存在
- 获取这个变量的值会报错xxx is not defined;,
- 给这个变量设置值,那么设置变量就是隐式全局变量。
全局作用域只要页面不卸载,就一直存在,不释放。
函数每次在调用时,都会形成一个作用域,当函数调用结束时,这个作用域就释放了。
函数闭包
闭包的概念
闭包的基本概念
If you can’t explain it to a six-year-old, you really don’t understand it yourself.
闭包(closure)是JavaScript语言的一个难点,也是JavaScript的一个特色,很多高级的应用都要依靠闭包来实现。
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数
在JavaScript中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。
闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用
【产生闭包的条件:当内部函数访问了外部函数的变量的时候,就会形成闭包。】
【当内部函数访问了外部函数的变量的时候,就会形成闭包。当一个函数访问另一个函数内的局部变量,只有在内外函数的情况。】
闭包的应用
计数器
1 | // 需求:统计一个函数的调用次数 |
私有变量
1 | // 使用闭包实现私有变量的读取和设置 |
实现缓存
缓存(cache):数据的缓冲区,当要读取数据时,先从缓冲中获取数据,如果找到了,直接获取,如果找不到,重新去请求数据。
计算斐波那契数列,会有很大的性能问题,因为重复的计算了很多次,因此我们可以使用缓存来解决这个性能问题。
初级优化:
使用缓存的基本步骤:
- 如果要获取数据,先查询缓存,如果有就直接使用
- 如果没有,就进行计算,并且将计算后的结果放到缓存中,方便下次使用。
1 | var arr = []; |
缺点:既然使用缓存,就需要保证缓存的数据的安全,不能被别人修改,因此,需要使用闭包来实现缓存的私有化。
1 | function outer() { |
闭包存在的问题
闭包占用的内存是不会被释放的,因此,如果滥用闭包,会造成内存泄漏的问题。闭包很强大,但是只有在必须使用闭包的时候才使用。
js的垃圾回收机制
引用计数法清除
1 | var o = { |
标记清除法清除
使用引用计数法进行垃圾回收的时候,会出现循环引用导致内存泄漏的问题。因此现代的浏览器都采用标记清除法来进行垃圾回收。
这个算法假定设置一个叫做根(root)的对象(在Javascript里,根是全局对象Window)。定期的,垃圾回收器将从根开始,找所有从根开始引用的对象,然后找这些对象引用的对象……从根开始,垃圾回收器将找到所有可以获得的对象和所有不能获得的对象。
从2012年起,所有现代浏览器都使用了标记-清除垃圾回收算法。
正则表达式
正则表达式:用于匹配规律规则的表达式,正则表达式最初是科学家对人类神经系统的工作原理的早期研究,现在在编程语言中有广泛的应用,经常用于表单校验,高级搜索等。
创建正则表达式
- 构造函数的方式
var regExp = new RegExp(/\d/);
- 正则字面量
var regExp = /\d/;
- 正则的使用
/\d/.test("aaa1");
普通元字符
/abc/
/123/
/:,汉字等/
- 点要用转义字符
\.
元字符
正则表达式由一些普通字符和元字符组成,普通字符包括大小写字母、数字等,而元字符则具有特殊的含义。
\d
[0-9]
数字字符\D
[^0-9]
非数字字符\s
[\f\r\n\t\v]
不可见字符\S
[^\f\r\n\t\v]
可见字符\w
[a-zA-Z0-9_]
word字符\W
[^a-zA-Z0-9_]
非word字符.
[^\n\r]
除了换行和回车之外的所有字符
|
表示或的意思
()
优先级最高,表示分组
字符类元字符
[a-z]
[1-9]
表示范围
[]
在正则表达式中表示一个字符的位置,[]
里面写这个位置可以出现的字符。
[^]
在中括号中^表示非的意思;表示该位置不可以出现的字符;
1 | console.log(/[abc]/);//匹配a,b,c; |
边界类元字符
^
表示以 ^
之后的字符开始,精确匹配
$
表示以 $
之前的字符结尾,精确匹配
1 | console.log(/^chuan/.test("dachuan"));//必须以chuan开头 |
量词类元字符
量词用来控制出现的次数,一般来说量词和边界会一起使用
*
表示出现0次或者多次,>=0
+
表示最少出现一次或者多次,>=1
?
表示只能出现0次或者1次,=0或者=1
{n}
表示出现n次,=n
{n,}
表示最少出现n次,>=n
{n,m}
表示可以出现n到m次,n<=x<=m
正则表达式修饰符
g
global
全局查找i
ignore
忽视大小写
正则表达式的使用
正则的使用:
- 字符串替换:
replace
正则的替换 - 字符串匹配:
match
匹配某个字符串中所有符合规律的字符串。 - 正则的测试:
test
表单校验,判断某个字符串是否符合正则的规律 - 正则的提取: 提取匹配的字符串的每一个部分。
()
进行分组
- 正则测试
.test(String);
测试字符串,是否满足正则表达式,返回值是布尔类型 - 正则提取
.exec(String);
提取满足正则表达式的字符串,返回值是一个数组 - 正则匹配
String.match(正则表达式g);
把所有满足的正则表达式的字符串返回,返回值是一个数组 - 正则替换
String.replace(正则表达式g,替换成xx);
把所有满足正则表达式的字符替换成xx
1 | 正则测试 |
Event loop
JavaScript是单线程
JavaScript是单线程的,在执行JavaScript代码时遇见定时器,事件等不是立即执行的代码,会交给浏览器(浏览器是多线程的),浏览器会开启一个队列,把JavaScript交给浏览器的代码,排列在队列中,在JavaScript执行完代码后,会将队列中的符合执行条件的代码执行;
运算符相关
=
赋值运算符
赋值运算符是一个表达式,这个表达式会有一个结果,是=右边的结果;相当于函数返回了一个结果;
赋值运算符会把一个值赋值给一个变量,这个表达式也会有一个结果;可以用 console.log
来打印这个结果测试;
(foo.bar = foo.bar) ()
这里是一个赋值运算表达式,给一个方法赋值了一个函数,但是括号内的这个运算表达式也产生了一个结果,就是右边的方法函数,所以会是函数调用;
,
逗号运算符
逗号运算符和赋值运算符一样;也产生了一个结果;