js高级
-
js变量提升和函数提升(预解析)
如果通过var声明变量:
- 如果一个声明的变量在函数体内,那么它的作用域就是函数内部。如果是在全局环境下声明的,那么它的作用域就是全局
- ES6之前没有块级作用域,只有函数作用域和全局作用域,ES6中提供了块级作用域
- 函数内部的变量会被提升到函数的头部,函数在解析执行的时候,先进行变量声明处理,然后在运行函数内部的代码
- 函数内部如果没有变量声明,那么该变量就是全局变量(如果函数内部没有var,该变量就是全局变量)
- 变量和赋值语句一起书写,在js引擎解析时,会将其拆成声明和赋值两部分,声明置顶,赋值保留在原来位置
- 变量重复声明不会出错,后面的会覆盖前面的
- 局部变量会随函数调用后释放
- 函数提升所指的形式:function fn(){…} 必须是函数声明的形式,不能是函数表达式的形式
- 函数提升的方式:将function fn(){…}整个函数声明代码块提升到当前作用域的顶部,原先位置以不存在该代码
作用域和作用域链
作用域:
用途: 一个变量的可用范围
- 保存局部变量,不会污染全局
1.在函数内var的
2.参数变量也是局部变量
作用域链
- 定义: 由多级作用域对象,逐级引用形成的链式结构
闭包
- 定义: 既重用一个变量,又保护对象不被污染的一种机制
1.用外层函数包裹,要保护的变量和使用变量的内层函数
2.外层函数返回内层函数
3.调用者,调用外层函数来获得内层函数对象
- 函数嵌套
- 使用函数内部的变量在函数执行完后,仍然存活在内存中(延长了局部变量的周期)
面向对象介绍
什么是对象?
对象到底是什么,我们可以从两个层次来理解。
(1)对象是单个事物的抽象
(2)对象是一个容器,封装了属性(property)和方法(method)
属性是对象的状态,方法是对象的行为(完成某种任务)
属性: 其实就是保存在对象中的普通方法对象的属性如果是复杂命名(xxx-xxx),或者不确定的时候,用对象[属性名]表示
- 对象中的方法可以简写为 方法名(){},不用写成方法名:function (){}
PS:每个对象都是基于一个引用类型创建的,这些类型可以是系统内置的原生类型,也可以是开发人员自定义的类型
什么是面向对象?
面向对象编程 —— Object Oriented Programming,简称 OOP,是一种编程开发思想。
我们在使用对象时,我们只关注对象的功能,不关注对象的内部细节
在面向对象开发思想中,每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。
因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发
面向对象与面向过程:
- 面向过程就是亲力亲为,事无巨细,面面俱到
而是为了描叙某个事物在整个解决问题的步骤中的行为
面向对象的特性:
- 封装性
对于一些功能相同或者相似的代码,我们可以放到一个函数中去,
多次用到此功能时,我们只需要调用即可,无需多次重写
类与类之间的关系,子类可以使用父类的所有功能,并且对这些功能进行扩展
相同行为,不同的实现
对象本身的状态与行为,以及对象之间的关系,都是抽象的结果;
把同类的对象共有的属性或方法抽出封装成单独的对象,在用到的时候给相应的对象使用
创建对象的方式:
- 通过Object构造函数
构造函数
- 创建多个结构相同的对象
通过new的方式创建实例发生了什么:
- 创建一个新对象;
工厂函数与构造函数的区别
- 没有显示的创建对象
判断一个对象的类型
- typeof: 返回所有基本类型 object function
console.log(s2.constructor === Student) // => true
console.log(s1.constructor === s2.constructor) // => true
console.log(s1 instanceof Student) // => true
console.log(s2 instanceof Student) // => true
构造函数和实例对象的关系
- 构造函数是根据具体的事物抽象出来的抽象模板
构造函数、原型对象、实例的关系
- 每个构造函数都有一个原型对象
思考:假设我所有的实例都有一个type属性和sayHi方法, 我应该怎么做最好?
原型
- prototype和__proto__每一个构造函数都有一个prototype属性,指向另一个对象。
function F () {}
console.log(F.prototype.constructor === F) // true
var instance = new F()
console.log(instance.proto === F.prototype) // => true
- 任何函数都具有一个 prototype 属性,该属性是一个对象
原型链
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性
搜索首先从对象实例本身开始
也就是说,在我们调用 student1.sayHi() 的时候,会先后执行两次搜索
- 首先,解析器会问:“实例person1有sayName 属性吗?” 答:“没有”
原型模式的执行流程
- 先查找构造函数实例里的属性或方法,如果有,立刻返回
总结
- 先在自己身上找,找到即返回
更简单的原型语法
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
type: ‘human’,
sayHello: function () {
console.log(‘我叫’ + this.name + ‘,我今年’ + this.age + ‘岁了’)
}
}
在该示例中,我们将 Person.prototype 重置到了一个新的对象。
这样做的好处就是为 Person.prototype 添加成员简单了
但是也会带来一个问题,那就是原型对象丢失了 constructor 成员
所以,我们为了保持 constructor 的指向正确,建议的写法是
function Person (name, age) {
this.name = name
this.age = age
}
Person.prototype = {
constructor: Person, // => 手动将 constructor 指向正确的构造函数
type: ‘human’,
sayHello: function () {
console.log(‘我叫’ + this.name + ‘,我今年’ + this.age + ‘岁了’)
}
}
原型模式的缺点
原型中所有属性是被很多实例共享的,共享对于函数非常合适,
对于包含基本值的属性也还可以。但如果属性包含引用类型,就存在一定的问题
function Box() {};
Box.prototype = {
constructor : Box,//指向构造函数
name : ‘Lee’,
age : 100,
family : [‘父亲’, ‘母亲’], //添加了一个数组属性
run : function () {
return this.name + this.age + this.family;
}
}
var box1 = new Box()
box1.family.push(‘弟弟’) //在实例中添加’哥哥‘
alert(box1.run())
var box2 = new Box()
alert(box2.run()) //共享带来的麻烦,也有’弟弟’了
组合模式
为了解决构造传参和共享问题,可以组合构造函数+ 原型模式
function Box(name, age) { //不共享的使用构造函数
this.name = name
this.age = age
this.family = [‘父亲’, ‘母亲’, ‘妹妹’]
}
Box.prototype = { //共享的使用原型模式
constructor : Box,
run : function () {
return this.name + this.age + this.family
}
}
var b1 = new Box(\'lp\', 20)var b2 = new Box(\'wq\', 22)b1.family.push(\'哥哥\')console.log(b1.run())console.log(b2.run())console.log(b1.family === b2.family)
原生对象的原型
- 所有函数都有 prototype 属性对象Object.prototype
原型对象使用建议
- 私有成员(一般就是非函数成员)放到构造函数中
this指向
- this关键字表示对某个对象的引用,可以把他理解为一个引用类型的变量,但它的值是系统确定的,也就是this无法赋值
在全局执行环境中使用this,表示global对象,在浏览器中就是window对象
var box = 2;
alert(this.box); //全局,代表 window
在函数执行环境中使用this,情况比较复杂,
如果函数没有明显的作为非window对象的属性,而只是定义了函数,不管这个函数是否定义在另一个函数中,
这个函数中的this仍然表示window对象。如果函数显式地作为一个非window对象的属性,那么函数中的this就表示这个对象
function greeting(){
this.name=“jack”;
alert(\”hello \”+this.name);
}
obj.fn();//obj对象的fn()方法中的this指向obj
var dog=new Dog();//构造函数内的this指向新创建的实例对象
使用面向对象编程
创建一个元素,并添加样式
面向过程的编程方式
var div = document.createElement(“div”);
div.style.width = “500px”;
div.style.height = “300px”;
div.style.backgroundColor = \”#f37f38”;
document.body.appendChild(div);
function CreateElement(tag) {
this.createDOM = document.createElement(tag)
}
CreateElement.prototype = {
constructor: CreateElement,
appendTo: function (node) {
node.appendChild(this.createDOM);
return this
},
css: function (data) {
for (var key in data) {
this.createDOM.style[key] = data[key]
}
return this
}
}
var pElement = new CreateElement(“div”);
pElement.css({
“width”: “300px”,
“height”: “300px”,
“backgroundColor”: “#30d05f”
}).appendTo(document.body)
继承
原型继承(将父类的实例作为子类的原型)
JS主要时通过原型链来实现继承,现在让一个对象的原型等于另一个对象的实例,
那么这个对象就继承了另一个对象的所有属性和方法,而这个对象的所有实例都共享这个对象的原型里面的所有属性及方法
function Person(name,age) {
this.name =name || ‘Tom’
this.age = age || 20
}
Person.prototype.tst1=function(){
alert(‘哈哈哈’)
}
function Student(sex) {
this.sex = sex || ‘男’
}
Student.prototype=new Person(‘xiao’,18)
var stu1=new Student(‘男’)
console.log(stu1.name)
console.log(stu1.age)
console.log(stu1.sex)
stu1.tst1()
特点:
非常纯粹的继承关系,实例是子类的实例,也是父类的实例
父类新增原型方法/原型属性,子类都能访问到
简单,易于实现
缺点:
要想为子类新增属性和方法,必须要在new Animal()这样的语句之后执行,不能放到构造器中
无法实现多继承
来自原型对象的所有属性被所有实例共享(来自原型对象的引用属性是所有实例共享的)
创建子类实例时,无法向父类构造函数传参
构造继承(使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型))
call apply继承
bind
改变this的指向 并且返回一个新的函数(不调用函数)
call
改变this的指向 直接调用函数 第一个参数(要改变this指向的对象) 第二个参数(参数列表)
apply
改变this的指向 直接调用函数 第一个参数(要改变this指向的对象) 第二个参数(参数数组)
call方法继承
function Person(name,age) {
this.name =name || ‘Tom’
this.age = age || 20
}
function Student(name,age,sex){
Person.call(this,name,age)
this.sex = sex || ‘男’
}
var stu=new Student(‘lucy’,18,‘女’)
alert(stu.name)
apply 方法继承
function Person(name,age) {
this.name =name || ‘Tom’
this.age = age || 20
}
function Student(name,age,sex) {
Person.apply(this,[name,age])
this.sex=sex
}
特点:
子类实例共享父类引用属性的问题
创建子类实例时,可以向父类传递参数
可以实现多继承(call多个父类对象)
缺点:
实例并不是父类的实例,只是子类的实例
只能继承父类的实例属性和方法,不能继承原型属性/方法
无法实现函数复用,每个子类都有父类实例函数的副本,影响性能
实例继承(为父类实例添加新特性,作为子类实例返回)
function Cat(name){
var instance = new Animal()
instance.name = name || ‘Tom’
return instance
}
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // false
- 特点:
不限制调用方式,不管是new 子类()还是子类(),返回的对象具有相同的效果
实例是父类的实例,不是子类的实例
不支持多继承
拷贝继承
function Cat(name){
var animal = new Animal()
for(var p in animal){
Cat.prototype[p] = animal[p]
}
Cat.prototype.name = name || ‘Tom’
}
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // false
console.log(cat instanceof Cat) // true
- 特点:
支持多继承
效率较低,内存占用高(因为要拷贝父类的属性)
无法获取父类不可枚举的方法(不可枚举方法,不能使用for in 访问到)
组合继承(通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用)
function Cat(name){
Animal.call(this)
this.name = name || ‘Tom’
}
Cat.prototype = new Animal()
组合继承也是需要修复构造函数指向的
Cat.prototype.constructor = Cat
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) // true
- 特点:
可以继承实例属性/方法,也可以继承原型属性/方法
既是子类的实例,也是父类的实例
不存在引用属性共享问题
可传参
函数可复用
调用了两次父类构造函数,生成了两份实例(子类实例将子类原型上的那份屏蔽了
寄生组合继承(通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性,避免的组合继承的缺点)
function Cat(name){
Animal.call(this)
this.name = name || ‘Tom’
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){}
Super.prototype = Animal.prototype
//将实例作为子类的原型
Cat.prototype = new Super()
Cat.prototype.constructor = Cat
})()
var cat = new Cat()
console.log(cat.name)
console.log(cat.sleep())
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat) //true
PS:最好的实现继承的方式
call、apply其他使用场景
- 数组最大最小值
var arr = [1, 33, 43, 765, 23, 99]
var min = Math.min.apply(null, arr)
var max = Math.max.apply(null, arr)
call apply第一个参数传null或者undefined 代表window
-
数组追加
var array1 = [12 , “foo” , {name:“Joe”} , -2458]
var array2 = [“Doe” , 555 , 100]
Array.prototype.push.apply(array1, array2)
function的apply方法的第二个参数是一个数组集合,
在调用的时候,他需要的不是一个数组,但是为什么他给我一个数组我仍然可以将数组解析为一个一个的参数
这个就是apply的一个巧妙的用处,可以将一个数组默认的转换为一个参数列表([param1,param2,param3] 转换为 param1,param2,param3)
所以利用变个特性,我们就实现了上面的功能。 -
验证是否是数组
functionisArray(obj){
return Object.prototype.toString.call(obj) === ‘[object Array]’
} -
把类数组转换为数组
Array.prototype.slice.call(类数组)
arguments对象
当一个函数要被执行时,系统会在执行函数体代码前做一些初始化的工作,
其中之一就是为函数对象创建一个arguments对象属性。Arguments 对象只能使用在函数体中,
并用来管理函数的实际参数。arguments 对象有一个Length属性。表示函数被调用时实际传递的参数个数
在函数代码中,使用特殊对象 arguments,开发者无需明确指出参数名,就能访问它们
– 检测参数个数
function howManyArgs() {
alert(arguments.length)
}
howManyArgs(“string”, 45)
howManyArgs()
howManyArgs(12)
- 模拟函数重载- 用 arguments 对象判断传递给函数的参数个数,即可模拟函数重载function doAdd() {if(arguments.length == 1) {alert(arguments[0] + 5)} else if(arguments.length == 2) {alert(arguments[0] + arguments[1])}}doAdd(10) //输出 \"15\"doAdd(40, 20) //输出 \"60\"当只有一个参数时,doAdd() 函数给参数加 5。如果有两个参数,则会把两个参数相加,返回它们的和所以,doAdd(10) 输出的是 \"15\",而 doAdd(40, 20) 输出的是 \"60\"