1. 执行上下文
我们知道,每当函数调用时都会创建新的执行上下文,这也就是说,执行上下文在代码执行时才确定。
- 在创建执行上下文这个过程中做了以下几件事:
- 创建变量对象(变量对象中保存着变量、函数声明以及形式参数(arguments),变量对象在函数执行时变成可访问的活动对象);
- 创建作用域链;
- 确定this指向;
- 它的类型:
- 全局执行上下文;
- 函数执行上下文;
- eval执行上下文;
- 代码执行过程:
- 创建全局上下文 (global EC);
- 全局执行上下文 (caller)逐行自上而下执行。遇到函数时,函数执行上下文 (callee) 被push到执行栈顶层;
- 函数执行上下文被激活,成为 active EC, 开始执行函数中的代码,caller 被挂起 函数执行完后,callee 被pop移除出执行栈,控制权交还全局上下文 (caller),继续执行;
2. 作用域(词法环境)
作用域在定义时就已经确定,区别于执行上下文。作用域决定了代码区块中变量和其他资源的可见性。
- 全局作用域;
- 函数作用域;
- 块级作用域(ES6新增let,const);
3. 作用域链
在代码执行时,会创建变量对象的一个作用域链,用途:保证对执行环境有权访问的所有变量和函数的有序访问。
作用域链的最前端,始终是当前执行的代码所在的执行上下文的变量对象,如果是函数执行上下文,则将其活动对象作为变量对象,全局执行上下文的变量对象始终都是作用域链的最后一个对象。搜索变量和函数名时,如果搜索不到则再搜索上一级作用域链。
4. 闭包
闭包是指能够访问另一个函数作用域中变量的函数,创建闭包的常见方式就是在一个函数内部创建另一个函数。
var a = 10
function getValue() {
let a = 20
return function() {
console.log(a)
}
}
getValue()() // 20
当外部函数执行,内部匿名函数被返回后,它的作用域链被初始化为包含外部函数的活动对象和全局变量对象,这样匿名函数就可以访问外部函数中的变量。更为重要的是,当外部函数执行完毕后,其活动对象也不会被销毁,因为匿名函数的作用域链仍然在引用这个对象。
5. this指向
this总是指向最后调用它的那个对象,这句话不知道是否严谨,但至少打通了我对this的认识,请看this详解
this对象是在运行时基于函数的执行上下文绑定的:
- 在全局函数中,this指向Window;
- 当函数作为某个对象的方法调用时,this指向那个对象;
- 匿名函数的执行上下文具有全局性,因此匿名函数中的this通常指向Window;
改变this指向:
getValue.call(Obj,val1,val2)getValue.apply(Obj, [val1, val2])getValue.bind(Obj)(val1, val2)- 箭头函数
- new关键字
很显然,我们能看出这几个方法的区别:
call和apply在改变this指向的同时立即执行函数,而bind并不立即执行;call和apply传递参数也有区别,call接收多个参数,apply接收一个数组
6. 原型和原型链
原型是一个对象(prototype),用于实现对象的继承,在火狐和谷歌浏览器中,对象都有一个__proto__属性指向该对象的原型对象。
原型链是由原型对象组成,每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的指针,那么当原型对象等于另一个类型的实例时,此时的原型对象将包含一个指向另一个原型的指针,如此便形成了原型的链条,这就是原型链的基本概念。
- 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined;
- 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。
7. 继承
-
原型链继承:
特点:
- 子类的原型是父类的实例
- 父类新增原型属性和原型方法子类都能访问到
问题:
- 包含引用类型值的原型属性会被所有实例共享
- 无法实现多继承
- 创建子类的实例时,不能像父类型的构造函数中传参
//父类 function Person(name, age) { this.name = name; this.age = age; } Person.prototype.getHobby = function() { return this.hobby; } // 子类 function Student(hobby) { this.hobby = hobby; } // 子类的原型是父类的实例 Student.prototype = new Person('zjx', 18); var student1 = new Student('LOL'); console.log(student1.getHobby()) // LOL -
构造函数继承
特点:
- 解决子类实例共享父类引用属性的问题
- 可实现多继承
- 可以在子类的构造函数中向父类构造函数传递参数
问题:
- 方法都在构造函数中定义,因此函数复用无从谈起
- 父类的原型中定义的方法在子类中无法访问
function Person(name) { this.name = name } function Student(val) { Person.call(this, val) } // 传参 var student1 = new Student('zjx') console.log(student1.name) // zjx -
组合继承
- 这种继承方式避免了以上两种的缺陷,融合了他们的优点,成为最常用的继承模式
缺点:
- 调用了两次父类构造函数
// 父类 function Person(name) { this.name = name } Person.prototype.getName = function() { return this.name } // 子类 function Student(name, age) { Person.call(this, name) // 第2次 this.age = age } // 子类的原型是父类的实例 Student.prototype = new Person() // 第1次 // 因为上一步修改了原型,所以constructor也被修改,这一步将constructor指回子类 Student.prototype.constructor = Student var student1 = new Student('zjx', 18) console.log(student1.name, student1.age, student1.getName()) // 'zjx' 18 'zjx' -
原型式继承
// 用对象person作为原型对象 var person = { name: 'zjx', hobby: ['pingpong', 'basketball'] } var student1 = Object.create(person, { name: { value: 'zy' } }) var student2 = Object.create(person, { name: { value: 'hml' } }) student2.hobby.push('LOL') // 这里看出问题了吧,包含引用类型的属性始终都会共享相应的值 console.log(student1.hobby, student2.hobby) // ['pingpong', 'basketball', 'LOL'] ['pingpong', 'basketball', 'LOL'] -
寄生式继承 寄生式继承的思路与工厂模式类似,即创建一个仅用于封装继承过程的函数,由于不能做到函数复用而降低效率,这一点与构造函数模式类似。
function createAnthor(obj) { var newObj = Object.create(obj); // Object.create()并不是必须的,任何可以返回对象的函数都适用于此模式 newObj.sayHi = function() { // 在这里增强对象 console.log('hi'); } return newObj; // 返回这个对象 } var person = { name: 'zjx', age: 18 } var anthorPerson = createAnthor(person); anthorPerson.sayHi(); // 'hi' -
寄生组合式继承
为了解决组合式继承调用了两次父类构造函数而存在的继承模式
function Person(name) { this.name = name; } Person.prototype.getName = function() { return this.name; } function Student(name, age) { Person.call(this, name); // 只在这里调用了一次父类构造函数 this.age = age; } // 继承原型 function inheritPrototype(subType, superType) { let prototype = object.create(superType.prototype); // 1.创建超类原型的副本 prototype.constructor = subType; // 2.为副本添加constructor属性 subType.prototype = prototype; // 3.将新对象赋给子类的原型 } inheritPrototype(Student, Student); Student.prototype.sayAge = function() { return this.age }
8. 创建对象
查看详情 虽然Object构造函数和对象字面量都看可以用来创建单个对象,但这些方式有明显的缺点:使用同一接口创建很多对象,会产生大量的重复代码,因此便又衍生出了下面这些创建对象的模式。
-
工厂模式
优点:
- 解决了创建多个相似对象的问题
缺点:
- 却没有解决对象的识别问题
function createPerson(name, age) { var o = new Object(); o.name = name; o.age = age; o.sayName = function () { return this.name; } return o; } var person1 = createPrson('zjx', 18) -
构造模式
优点:
- 解决了对象的识别问题
缺点:
- 每个实例都会创建不同的function实例,而其实创建完成同样任务的function实例是很没有必要的
function Person(name, age) { this.name = name this.age = age this.sayName = function() { return this.name } } var person1 = new Person('zjx', 18) -
原型模式
优点:
- 不用为构造函数传递参数,可以创建多个相同的对象
缺点:
- 原型中的属性被很多实例共享,当属性为包含引用类型值的属性时,修改一个实例中属性的值,另一个实例中的属性的值也会改变
function Person() { } Person.prototype.name = 'zjx'; Person.prototype.age = 18; Person.prototype.sayName = function() { return tihs.name; } var person = new Person(); -
组合模式 优点:
- 集构造模式与原型模式之所长,也是当前使用最广泛的模式
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.sayName = function() { return this.name; } var person1 = new Person('zjx', 18); -
动态原型模式
优点:
- 这种模式将所有信息都封装在了构造函数里,因为在组合构造函数模式和原型模式中,构造函数和原型模式是独立的,通过在构造函数中初始化原型,又保持了同时使用构造函数和原型的优点,换句话说,就是可以通过在构造函数中,检查某个应该存在的方法是否有效,来决定是否需要初始化原型
funtion Person(name, age) { this.name = name; this.age = age; if(typeof this.sayName != 'function') { // 判断是否存在该方法来决定是否需要初始化原型 Person.prototype.sayName = function() { return this.name; } } } var person1 = new Person('zjx', 18); -
寄生构造函数模式
这种模式其实和工厂模式很像,除了使用new操作符并把使用的包装函数叫做构造函数之外,和工厂模式可以说是一模一样的,这种模式的基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。
那么既然有了工厂模式,为什么还要有寄生构造函数模式呢?其实这个模式主要是用来给js原生的构造函数定义一些新的方法,我们可以看下面这个例子:
function SpecialArray() { var values = new Array(); values.push.apply(values, argumens); values.toPipedString = function() { return this.join("|"); } return values; } var colors = new SpecialArray("red","blue"); console.log(colors.toPipedString()); // red|blue -
稳妥构造模式
- 新创建对象的实例方法不应用this;不适用new操作符调用构造函数;实例中的属性除了实例方法可以访问以外,没有其他的办法访问实例属性的值。
function Person(name, age) { var o = new Object(); _name = name; _age = age; o.sayName = function() { return _name; } return o; } var person1 = Person("zjx", 29); person1.sayName(); // Nicholas -
ES6 Class
9. new运算符执行过程
- 创建一个空对象;
- 将新对象的原型指向构造函数的原型;
- 绑定this为当前对象;
- 返回这个新对象(如果构造函数有自己的返回值,且该返回值为对象时,则返回这个值;如果返回值不为对象,则返回刚创建的这个新对象);
10. 对象拷贝
- 浅拷贝: 以赋值的形式拷贝引用对象,仍指向同一个地址,修改时原对象也会受到影响
- 具有局限性的深拷贝:在没有耳机属性的情况下,一下这两种方法属于深拷贝,在对象层级较深的情况下,他们依然只能做浅拷贝
- Object.assign
- 展开运算符(...)
- 深拷贝: 完全拷贝一个新对象,修改时原对象不再受到任何影响
- JSON.parse(JSON.stringify(obj))
- 具有循环引用的对象时,报错
- 当值为函数、undefined、或symbol时,无法拷贝
- 递归
function deepClone (target) { let result; if (typeof target === 'object') { if (Array.isArray(target)) { result = [] for (let i in target) { result[i] = deepClone(target[i]) } } else if (target === null) { result = null } else if (target.constructor === RegExp) { result = target; } else { result = {} for (const i in target) { result[i] = deepClone(target[i]) } } } else { result = target } return result } - JSON.parse(JSON.stringify(obj))
最后要说一句,在实际生产环境中,我们还是要借助lodash的deepClone或者其他已封装好的方法,上面我们所写的方法还有很多没有考虑到的地方,比如ArrayBuffer什么得我们都没有进行处理。