JavaScript 进阶
给大家推荐一个实用面试题库
1、前端面试题库 (面试必备) 推荐:★★★★★
地址:web前端面试题库
1.原型链入门
1) 构造函数
- 当我们自定义一个函数时(箭头函数与生成器函数除外),这个函数就默认为一个构造函数【虽然它可以当做普通函数来使用】。
- 约定:
- 构造函数通常约定首字母大写;函数对象约定首字母小写;
- 构造函数调用的时候需要在前面加个 new 操作符;函数对象不需要。
==*new 一个函数的时候函数会被调用,不会return;如果构造函数没有形参,不需要加()和不加括号的调用过程一样的==
- 并不是所有的内置函数都是构造函数; 也并不是所有的自定义函数都是构造函数。
- 使用
new
命令时,它后面的函数依次执行下面的步骤。
- 创建一个空对象,作为将要返回的对象实例。
- 将这个空对象的原型,指向构造函数的
prototype
属性。 - 将这个空对象赋值给函数内部的
this
关键字。 - 开始执行构造函数内部的代码。
2) prototype、__ proto __、constructor
- prototype 被称为显式原型【通常是我们自己写,自己设置的,主要针对构造函数】
- __ proto __ 被称为隐式原型【自己生成的, 一般不会去更改,但是可以改,主要针对实例化对象】
- constructor 被称为构造器【通常是prototype的反方向,主要针对构造函数的显式原型】
==constructor
方法在创建对象实例时被自动调用,它的主要作用是初始化对象的属性和执行任何其他的准备工作。 可以将constructor
方法看作是类的构造函数,它定义了创建对象时需要执行的代码。==
- ==为显示原型里面设置的元素会在new后以隐式原型的形式出现。==
规则:
- 每个构造函数都有 prototype 和 __ proto __
function Fun() {} // undefined Fun.prototype // {constructor: ƒ} Fun.__proto__ // f () { [native code] }
- 每一个对象/构造函数实例(这个也是对象)都有 __ proto_ __ ,没有prototype
function Fun() {} // undefined fun = new Fun(); // Fun {} fun.__proto__; // {constructor: ƒ} fun.prototype // undefined 可以看出fun没有prototype
- 对象的 __ proto __ 指向它本身构造函数的prototype,这个称为构造函数的原型对象
function Fun() {} // undefined fun = new Fun(); // Fun {} fun.__proto__ == Fun.prototype // true 对象的__proto__ 指向了构造函数的prototype
- 调用对象/函数属性的时候,js引擎会沿着 __ proto __ 的顺序一直往上方查找,找到window.Object.prototype 为止,Object 为原生底层对象,到这里就停止了查找, 如果没有找到,就会报错或者返回 undefined
function Fun(){} // undefined var fun = new Fun(); // undefined fun.xxx // undefined 因为xxx不存在所以返回undefined, 如果没有找到它会跟着原型链进行查找 fun.__proto__ // {constructor: ƒ} 没有xxx继续向下一个__proto__查找 fun.__proto__.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} // fun.__proto__.__proto__.__proto__ 没有xxx继续再向下一个__proto__查找 // null 因为继承的关系, 所有对象都继承自Object, 如果还是没有xxx就返回null
- 构造函数(也就是函数)的 __ proto __ 指向 Function.prototype,它返回" ƒ () { [native code] } " (空函数,我习惯把它叫构造器函数);当然 Function也是一个函数,所以Function.__ proto __也指向空函数
function Fun(){} // undefined Fun.__proto__ // ƒ () { [native code] } Fun构造函数的prototype返回一个空函数 Function.prototy pe // ƒ () { [native code] } Function构造函数的prototype返回一个空函数 Function // ƒ Function() { [native code] } Function本身是一个函数 Function.__proto__ == Function.prototype // true 所以Function的隐式原型等于Function的显示原型
- 空函数的 __ proto __ 指向 Object.prototype, 即空函数" ƒ() { [native code] } " 的隐式原型指向Object的显示原型
function Fun(){} // undefined Fun.__proto__ // ƒ () { [native code] } 空函数 Function.prototype // ƒ () { [native code] } 空函数 Object // ƒ Object() { [native code] } 可以看出Object也是一个函数 Object.prototype // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} Fun.__proto__.__proto__ // {constructor: ƒ, __defineGetter__: ƒ, __defineSetter__: ƒ, hasOwnProperty: ƒ, __lookupGetter__: ƒ, …} Fun.__proto__.__proto__ == Object.prototype // true 空函数的隐式原型指向Object显示原型
- __ proto __是浏览器厂商实现的,W3C规范中并没有这个东西【所以不同浏览器的表现可能不同】
- constructor(构造器)是 prototype 的反向。
function Fun(){}; // undefined Fun.prototype.constructor // ƒ Fun(){} Fun.prototype.constructor == Fun // true 可以看出构造函数Fun的显示原型的构造器等于构造函数本身
- 任何函数的构造器都是Function,Function也不列外
function Fun(){}; // undefined Fun.constructor // ƒ Function() { [native code] } 函数的构造器数是Function Function // ƒ Function() { [native code] } Function本身是一个函数 Function.constructor // ƒ Function() { [native code] } Function的构造器是Function Function.constructor == Function // true 可以看出Function函数的构造器就是它本身 Fun.constructor == Function // true 得出Fun函数的构造器也是Function
- 实例化对象的构造器返回的本对象的构造函数
function Fun(){} // 定义Fun函数 // undefined fun = new Fun(); // 实例化一个fun对象 // Fun {} fun.constructor // 等同于 fun.__proto__.constructor // ƒ Fun(){} fun.constructor == Fun // true // 原理是构造函数的显示原型里边有一个构造器, 当创建对象之后, 对象就会去找隐式原型里边的构造器返回构造函数本身
- 对于很多原生构造函数(实例化对象)以 Number为例:
(1).__ proto __ 指向 Number.prototype
但是 Number.prototype 也是一种 Number的实例化对象
所以 (1).__ proto __.constructor == Number.prototype.constructor
但是注意: (1).__ proto . proto __ 指向的是 Object.prototype
(1).__proto__ == Number.prototype // true 可以近似看成 (1) ~= new Number(1) 这样 Number.prototype // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …} 看上去也是一个对象 (1).__proto__.constructor == Number.prototype.constructor // true (1).__proto__.__proto__ == Number.prototype.__proto__ // true (1).__proto__.__proto__ == Object.prototype // true 是因为继承关系 // 可以理解为原生的构造函数在浏览器上已经已经实例化为我们封装好了一些常用的方法, 所以Number.prototype看上去也是一个对象
3) instanceof
- 用于检测构造函数的prototype 属性是否出现在某个实例对象的原型链上
- 语法:
object instanceof constructor
- object:某个实例对象
- constructor:某个构造函数
function Fun() {} // undefined fun = new Fun() // Fun {} fun instanceof Fun // true
2.this
this
是一个关键字,在浏览器中,全局的this指向的是window;而node.js中this指向的是global。this
的五种绑定方式 :
- 默认绑定(非严格模式下this指向全局对象,严格模式下this会绑定到undefined)
- 隐式绑定(当函数引用有上下文对象时,如 obj.foo()的调用方式,foo内的this指向obj)
- 显式绑定(通过call()或者apply()方法直接指定this的绑定对象,如foo.call(obj))
- new绑定(针对new关键词的绑定,当构造函数被实例化后的绑定形式)
- 箭头函数绑定(this的指向由外层作用域决定的)
- 严格模式:严格模式是在 ECMAScript5(ES5)中引入的,在严格模式下,JavaScript 对语法的要求会更加严格,一些在正常模式下能够运行的代码,在严格模式下将不能运行。
- JavaScript 脚本的开头添加
"use strict";
或'use strict';
指令即可开启js严格模式代码。 - 严格模式的特征:
- 不允许使用未声明的变量
- 不允许删除变量或函数
- 函数中不允许有同名的参数
- eval 语句的作用域是独立的
- 不允许使用 with 语句
- 不允许写入只读属性
- 不允许使用八进制数(只禁用了 0,没有禁用 0o)
- 不能在 if 语句中声明函数
- 禁止使用 this 表示全局对象(但是全局this不受影响,只影响函数内部的this)
1) 默认绑定
// 1.非严格模式 (function f() { console.log(this) }()) // Window {window: Window, self: Window, document: document, name: 'window', location: Location, …} // 严格模式 'use strict'; (function f() { console.log(this) }()) // undefined // 【总结:】当函数没有所属对象直接调用时,this指向的是全局对象。 // ================================================================================================================ // 2.典例 // eg1: debugger ; var a = 1; function foo() { var a = 2; console.log(this); console.log(this.a); } foo() // 因为foo函数调用时处于全局环境下(全局window) // Window {window: Window, self: Window, document: document, name: 'window', location: Location, …} // 1 // eg2: debugger ;var a = 1; function foo() { var a = 2; function inner() { console.log(this.a) } inner() } foo() // 因为foo函数调用时处于全局环境下(全局window) // 1 //【总结:】环境对象的this: 谁调用我,我就指向谁
2) 隐式绑定
// 1.将函数放到对象中 debugger ;function foo() { console.log(this.a) } var obj = { a: 1, fun: foo }; var a = 2; obj.foo(); // 1 obj的fun属性值指向foo函数,而fun的调用体是obj对象,此时的this就指向obj对象。 // 等同于 var obj = { a: 1, foo: function() { console.log(this.a) } }; var a = 2; obj.foo() // js引擎会将函数单独保存在内存中,然后再将函数地址赋值给foo属性的value属性; // { // foo: { // [[value]]: 函数的地址 // ... // } // } // 由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。 // ================================================================================================================ // 2.隐式丢失 // 隐式丢失其实就是被隐式绑定的函数在特定的情况下会丢失绑定对象。 // 当使用另一个变量来给函数取别名或者将函数作为参数传递时会被隐式赋值,回调函数丢失this绑定 // 隐式丢失案例 var obj = { name: '金誉', info: function () { return '姓名:'+ this.name; } }; var name = 'tzw'; var f = obj.info; // 只要函数被赋给另一个变量,this的指向就会变 f() // "姓名:tzw" obj.info被赋值给变量f,内部的this就会指向f运行时所在的对象 // ================================================================================================================ // 典例 // eg1: var val = 10; obj = { val: 1, fun: function() { console.log(this) // {val: 1, fun: ƒ} this指向调用对象 var foo = function(){ console.log(this); // Window {window: Window, self: Window, document: document, name: 'window', location: Location, …} this指向window,匿名函数赋值给了变量foo,回调函数丢失this绑定就指向了全局window console.log(this.val); // 10 取得是全局window对象的val属性值 } foo(); return this.val // 1 fun是通过obj对象去调用的, 所以返回的this.val指向obj对象的val } } obj.fun(); // ================================================================================================================ // eg2: debugger ;function foo() { console.log(this.a); // 2 在函数deFOO调用时, obj对象的foo属性作为实参, foo本身是一个函数, 将函数作为参数传递时this会丢失,所以this.a就指向了全局的变量a } function doFoo(fn) { console.log(this); // {a: 3, doFoo: f} 因为是obj2对象调用的,所以this指向obj2对象 fn() } var obj = { a: 1, foo }; var a = 2; var obj2 = { a: 3, doFoo }; obj2.doFoo(obj.foo) //【总结:】函数的定义位置不影响其this指向,this指向只和调用函数的对象有关; // 多层嵌套的对象,内部方法的this指向离被调用函数最近的对象。
3) 显式绑定
==在逆向中极其极其极其常用,补环境,抠代码读代码都常用==
- 可以强行使用某些方法,改变函数内this的指向
- 通过call()、apply()或者bind()方法直接指定this的绑定对象
- 使用.call()或者.apply()的函数是会直接执行的
- bind()是创建一个新的函数,需要手动调用才会执行
- 如果call、apply、bind接收到的第一个参数是空或者null、undefined的话,则会忽略这个参数。
debugger ;function foo() { console.log(this.a) } var a = 2; foo.call(); // 2 foo.call(null); // 2 foo.call(undefined) // 2
(1) call()
call(对象, arg1, arg2...)
第一个参数是新的this指向,从arg1
参数开始之后是传递给参数的实参,可以是数字、字符串和数组等类型的数据类型都可以。
debugger;const obj = { a: 100 }; function sum(x, y) { console.log(this.a + x + y); } sum(1, 2) // underfined+1+2=NaN this指向window,window下面没有a属性,所以window.a是undefined sum.call(obj, 1, 2) // 100+1+2=103 通过call改变this,让其指向obj,obj下面具有a属性,所以obj.a是100
(2) apply()
apply(对象, [arg1, arg2...])
第一个参数就是新的this指向, 第二个参数是一个数组或者类数组,里面的值依然是函数自身的参数。
debugger ;var obj = { name: 'tzw' }; window.name = 'window'; var getName = function(age) { alert(this.name); console.log('姓名:' + this.name + '\n' +'年龄:' + age) }; getName(); // 姓名:window 年龄:undefined this指向window,没有传参所以age是undefined getName.apply(obj, [26]) // 姓名:tzw 年龄:26 通过apply改变this,让其指向obj下面的name属性,所以name是tzw // 注意中括号是apply的第二个参数必须是数组或者类数组。
(3) bind()
bind(对象, arg1, arg2...)
语法和call()
一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8- bind 返回的是一个函数体(新的函数),并不会执行函数
debugger; window.name = 'tzw'; const obj = { name: 'qdd' } function fun() { console.log(this.name); } fun.bind(obj) // fun() { console.log(this.name); } 函数没有调用返回一个新函数 fun.bind(obj)() // 'qdd'
// 典例 // eg1: debugger ; var obj2 = { a: 2, foo1: function() { console.log(this.a) }, foo2: function() { setTimeout(function() { console.log(this); console.log(this.a) }, 0) } }; var a = 3; obj2.foo1(); // 2 obj2.foo2(); // setTimeout 是浏览器自带对象方法,所以this指向了window对象 // Window {window: Window, self: Window, document: document, name: 'tzw', location: Location, …} // 3 // =============================================================================================================== // eg2: debugger ;var obj1 = { a: 1 }; var obj2 = { a: 2, foo1: function() { console.log(this.a) }, foo2: function() { setTimeout(function() { console.log(this); console.log(this.a) }.call(obj1), 0) } }; var a = 3; obj2.foo1(); // 2 obj2.foo2(); // {a:1} // 1 // =============================================================================================================== // eg3: debugger ;var obj1 = { a: 1 }; var obj2 = { a: 2, foo1: function() { console.log(this.a) }, foo2: function() { function inner() { console.log(this); console.log(this.a) } inner() } }; var a = 3; obj2.foo1(); // 2 obj2.foo2(); // Window {window: Window, self: Window, document: document, name: 'tzw', location: Location, …} // 3 // =============================================================================================================== // eg4: debugger ;var obj1 = { a: 1 }; var obj2 = { a: 2, foo1: function() { console.log(this.a) }, foo2: function() { function inner() { console.log(this); console.log(this.a) } inner.call(obj1) } }; var a = 3; obj2.foo1(); // 2 obj2.foo2() // {a: 1} // 1 // =============================================================================================================== // eg5: debugger ;function foo() { console.log(this.a); return function() { console.log(this.a) } } ;var obj = { a: 1 }; var a = 2; foo(); // 2 foo.bind(obj); // 不会打印在控制台上,中间被吞了的原因是因为控制台只会返回最后一个 foo().bind(obj) // 2 // (){console.log(this.a)}
4) new绑定
- 当使用 new 关键字调用函数时,函数中的 this 一定是js创建的新对象
- 使用new调用函数时,会执行如下步骤:
- 创建(或者构造)一个全新的对象
- 这个新对象会被执行
[[prototype]]
连接 - 这个新对象会绑定到函数调用的this
- 如果函数没有返回其它对象,那么表达式中的函数调用会自动返回这个新对象。
- 一句话概括就是:当用 new 运算符调用函数时,该函数总会返回一个对象,通常情况下,构造函数里的this就指向返回的这个对象。
debugger; let Myclass = function() { this.name = 'tzw'; }; let obj = new Myclass(); // 构造函数里的this就指向返回的这个对象 obj.name; // 'tzw' // *如果构造函数显式地返回了一个 object 类型的对象,那么此次运算结果最终会返回这个对象,而不是我们之前的this。 let Info = function(){ this.name = 'qdd'; return { //显式地返回一个对象 name: 'tzw' } }; let obj = new Info(); obj.name; // 'tzw' // *如果构造函数不显式地返回任何数据,或者是返回一个非对象类型的数据,就不会造成上述问题,主要是new关键字调用函数,函数内部隐式返回this造成的。
// 典例 // eg1: debugger ;function Person(name) { this.name = name; this.foo1 = function() { console.log(this.name) } ; this.foo2 = function() { return function() { console.log(this.name) } } } ;var person1 = new Person('person1'); person1.foo1(); // 1 person1.foo2()(); // '' // *wind.name 不会被回收,刷新页面依然还在,有些网站会设置这个属性并检测 // =============================================================================================================== // eg2: debugger ;var name = 'window'; function Person(name) { this.name = name; this.foo = function() { console.log(this.name); return function() { console.log(this.name) } } } ;var person1 = new Person('person1'); var person2 = new Person('person2'); person1.foo.call(person2)(); // person2 // window person1.foo().call(person2); // person1 // person2
5) 箭头函数绑定
- 创建箭头函数时,就已经确定了它的 this 指向。
- 箭头函数没有自己的this指向,它会捕获自己定义所处的外层执行环境,并且继承这个this值,指向当前定义时所在的对象。箭头函数的this指向在被定义的时候就确定了,之后永远都不会改变。即使使用 call()、 apply() 、 bind()等方法改变this指向也不可以。
- 箭头函数的重要特征:箭头函数中没有this和arguments。
debugger ;var obj = { name: 'obj', foo1: ()=>{ console.log(this.name) } , foo2: function() { console.log(this.name); return ()=>{ // 指向外层作用域 console.log(this.name) } } }; var name = 'window'; obj.foo1(); // window foo1的对象是obj, obj是全局下定义的对象 obj.foo2()(); // obj // obj // 因为返回的是箭头函数,而它又是foo2属性的匿名函数的返回值,所以指向了foo2属性当前作用域下的name属性 obj2 = { name: 'obj2' }; obj.foo1.call(obj2); // window 因为call对箭头函数没有影响 //【总结:】箭头函数内的this是由外层作用域决定的 // 补充,如果箭头函数被赋给了一个变量 // function Fun() { this.name = () => { console.log(this); } // 等同于 this.name = function() {console.log(this);} } fun = new Fun(); fun.name() // Fun {name: ƒ} 这个this指向构造函数本身
原型链中的this: this这个值在一个继承机制中,仍然是指向它原本属于的对象,而不是从原型链上找到它时,它 所属于的对象。
总结
1.函数外面的this,即全局作用域的this指向window
**2.函数里面的this总是指向直接调用者;如果没有直接调用者,隐含的调用者是window **
**3.用new调用一个函数,这个函数即为构造函数。构造函数里面的this是和实例对象沟通 的桥梁,它指向实例对象 **
**4.事件回调里面,this指向绑定事件的对象,而不是触发事件的对象。当然这两个可以是一样的 **
5.箭头函数内的this由外层作用域决定
简单来说 this 的指向跟函数的调用位置紧密相关,要想知道函数调用时 this 到底引用了什么,就应该明确函数的调用位置。
3. 面向对象
1) 封装
- 封装就是将变量和方法包装在一个单元中,其唯一目的是从外部类中隐藏数据。这使得程序结构更易于管理,因为每个对象的实现和状态都隐藏在明确定义的边界之后。
- 封装,在ES6之前的使用的是构造函数,ES6之后用的是class【写前端常用但是逆向少见】
ES6的class实际就是一个语法糖,在ES6之前,是没有类这个概念的,因此是借助于原型对象和构造函数来实现。
- 私有属性和方法:只能在构造函数内访问不能被外部所访问(在构造函数内使用var等声明的属性)
- 公有属性和方法(或实例方法):对象外可以访问到对象内的属性和方法(在构造函数内使用this设置,或者设置在构造函数原型对象上比如Cat.prototype.xxx)
- 静态属性和方法:定义在构造函数上的方法(比如Foo.xxx),不需要实例就可以调用
// 1.静态属性方法和公有属性方法 debugger ;function Foo(arg1, arg2) { var private1 = 'pri1'; // *在函数内用var等定义的就是私有的 var private2 = 'pri2'; var private3 = function() { console.log(private1 + private2) }; this.pub1 = arg1; //*在函数内用this承接的就是公有的 this.pub2 = arg2; this.pub3 = function() { private3(); console.log('finish') } } Foo.descript = '1这是一段讲述静态属性方法的代码1'; Foo.descript2 = function() { console.log('2这是一段讲述静态属性方法的代码2') } ; Foo.prototype.descript3 = function() { console.log('1这是一段讲述公有属性方法的代码1') } ; var foo = new Foo('arg1','arg2'); console.log(Foo.descript); // 1这是一段讲述静态属性方法的代码1 Foo.descript2(); // 2这是一段讲述静态属性方法的代码2 console.log(foo.descript); // undefined foo.descript3(); // 1这是一段讲述静态属性方法的代码1 // 在构造函数上也就是使用Foo.xxx定义的是静态属性和方法,静态属性指的是Class本身的属性,而不是定义在实例对象(this)上的属性。 // 在构造函数内使用this设置,或者设置在构造函数原型对象上比如Foo.prototype.xxx,就是公有属性和方法(实例方法) // =============================================================================================================== // 2.定义在构造函数原型对象上的属性和方法不能直接表现在实例对象上,但是实例对象可以访问或者调用它们 // =============================================================================================================== // 3. 在ES6之后,新增了class 这个关键字。它可以用来代替构造函数,达到创建“一类实例”的效果; // 并且类的数据类型就是函数,所以用法上和构造函数很像,直接用new命令来配合它创建一个实例: // 类的所有方法都定义在类的prototype属性上面。 debugger ;class Foo { constructor() { // constructor()方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法; // 一个类必须有constructor()方法,如果没有显式定义,一个空的constructor()方法会被默认添加; // constructor()方法默认返回实例对象(即this),完全可以指定返回另外一个对象。 var p1 = 'luck'; this.p2 = 'foo'; this.p3 = function() {} }; p4 = 'white'; p5 = function() { console.log('我追你如果我追到你') }; p6() { // *这个方法定义当前类的原型上 console.log('我就把你嘿嘿嘿') } } var foo = new Foo(); console.log(foo); // { p4: 'white', p2: 'foo', p3: f, p5: f} foo.p5(); // '我追你如果我追到你' foo.p6(); // '我就把你嘿嘿嘿' // =============================================================================================================== // 4.可以使用static标识符表示它是一个静态的属性或者方法, // 加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。 // =============================================================================================================== // 5.class的变量不会提升 // =============================================================================================================== // 6.作用域 debugger ;class Cat { constructor() { this.name = 'guaiguai'; var type = 'constructor' } type = 'class'; // *这里的type是新写法,等同于this.type, 新写法定义的属性是实例对象自身的属性,而不是定义在实例对象的原型上面; // 不带this的写法的优先级比带this的优先级高 getType = function() { console.log(this.type); console.log(type) // 这里的type只的是全局下的 } } var type = 'window'; var guaiguai = new Cat(); guaiguai.getType(); // 'class' // 'window' 去全局里边的取得type, class的type属性是cat的原型对象里边的 // =============================================================================================================== // 7.闭包加深 debugger ;class Cat { constructor() { this.name = 'guaiguai'; var type = 'constructor'; this.getType = ()=>{ console.log(this.type); console.log(type) } } type = 'class'; getType = ()=>{ console.log(this.type); console.log(type) } } var type = 'window'; var guaiguai = new Cat(); guaiguai.getType(); // constructor this.getType()取得是闭包里边携带的type变量 console.log(guaiguai); // Cat {type: 'class', name: 'guaiguai', getType: ƒ}
2) 继承
- 继承是指从多种实现类中抽象出一个基类,使其具备多种实现类的共同特性。比如从猫类、狗类、虎类中可以抽象出一个动物类,具有猫、狗、虎类的共同特性(吃、跑、叫等)。
(1) 原型链继承
function Parent () { this.name = 'Parent' this.sex = 'boy' } Parent.prototype.getName = function () { console.log(this.name) console.log(this.sex) } function Child () { this.name = 'child' } Child.prototype = new Parent() // 将Child.prototype 指向Parent的实例 child1 = new Child() // 创建了一个Child对象child1 // Child {name: 'child'} child1.__proto__.__proto__ == Parent.prototype // true child1对象的原型(__proto__)指向了Child.prototype, 而Child.prototype的原型(__proto__)指向了Parent.prototype child1.getName() // child // 这个this.name 是从child1对象获取的 // boy // child1对象没有sex属性, console.log(child1) // Child {name: 'child'} [[Prototype]]: { Parent name: "Parent" sex: "boy" } [[Prototype]]: Object child1.sex = 'girl' // 直接修改对象上的属性,是给本对象上添加一个新属性,不会修改原型引用上的sex console.log(Child.prototype.__proto__ == Parent.prototype) // true // 这种方式就叫做原型链继承,将子类的原型对象指向父类的实例 // =============================================================================================================== // 缺陷 function A () {} function B () { this.name = 'anlan' } function C () {} B.prototype = new C() A.prototype = new B() a = new A() A.prototype.__proto__ == B.prototype // true a.__proto__ // C {name: 'anlan'} // 这是一个显示问题,正常来讲a.__proto__ 指向的应该是B的原型 // =============================================================================================================== function Parent (name) { this.name = name this.sex = 'boy' this.colors = ['white', 'black'] // 引用类型,取得是内存地址 } function Child (name) { this.name = name this.feature = ['cute'] } var parent = new Parent('parent') Child.prototype = parent var child1 = new Child('child1') child1.sex = 'girl' // 是给本对象上添加一个新属性,不会修改原型引用上的sex child1 // {"name": "child1", "feature": ["cute"],"sex": "girl"} child1.colors.push('yellow') // Parent原型的colors属性新增了一个yellow parent // {"name": "parent", "sex": "boy", "colors": ["white", "black", "yellow"]} child1.feature.push('sunshine') // child1对象本身有feature,所以是在本对象的属性上添加了一个sunshine child1 // {"name": "child1", "feature": ["cute", "sunshine"],"sex": "girl"} var child2 = new Child('child2') child2 // Child {name: 'child2', feature: Array(1)}feature : ['cute']name : "child2" [[Prototype]]:Parent colors : (3) ['white', 'black', 'yellow']name : "parent"sex : "boy" [[Prototype]]:Object 可以看出原型对象的所有属性都被共享了 // isPrototypeOf() 方法用于检查一个对象是否存在于另一个对象的原型链中。 // isPrototypeOf 实际上是instanceof 的反向。它是用来判断指定对象object1是否存在于另一个对象object2的原型链中,是则返回true,否则返回false。 Child.prototype.isPrototypeOf(child1) // true Parent.prototype.isPrototypeOf(child1) // true Object.prototype.isPrototypeOf(child1) // true // 原型链继承优缺点 // 优点:继承了父类的模板,又继承了父类的原型对象 // 缺点:1. 如果要给子类的原型上新增属性和方法,就必须放在Child.prototype = new Parent()这样的语句后面 // 2. 无法实现多继承, 多个原型指向同一个实例,当有多个实例化子对象时,修改一个会影响其他对象 // 3. 来自原型对象的所有属性都被共享了【浅拷贝】 // 4. 创建子类时,无法向父类构造函数传参数
(2) 构造继承
function Parent (name) { this.name = name } function Child () { this.sex = 'boy' Parent.call(this, 'child') // 这一步操作相当于把Parent里边的this.name作为Child的属性并赋值, this.name = 'child' // 这里的call换成apply和bind也可以,同样的效果 } var child1 = new Child() console.log(child1) // {"sex": "boy", "name": "child"} // =============================================================================================================== // 变种 function Parent (name) { this.name = name } function Child () { this.sex = 'boy' Parent.call(this, 'good boy') // 等同于 this.name = 'good boy' this.name = 'bad boy' // 相当于给this.name进行重新赋值 } var child1 = new Child() console.log(child1) // {sex: 'boy', name: 'bad boy'} // =============================================================================================================== // 涉及引用类型 function Parent (name, sex) { this.name = name this.sex = sex this.colors = ['white', 'black'] } function Child (name, sex) { Parent.call(this, name, sex) } var child1 = new Child('child1', 'boy') child1.colors.push('yellow') child1 // {"name": "child1", "sex": "boy", "colors": ["white", "black", "yellow"]} var child2 = new Child('child2', 'girl') child2 // {"name": "child2", "sex": "girl", "colors": ["white", "black"]} // child1和child2可以看出,是一个类似深拷贝的过程 // =============================================================================================================== function Parent (name) { this.name = name } Parent.prototype.getName = function () { console.log(this.name) } function Child () { this.sex = 'boy' Parent.call(this, 'good boy') } Child.prototype.getSex = function () { console.log(this.sex) } var child1 = new Child() console.log(child1) // {sex: 'boy', name: 'good boy'} child1.getSex() // 'boy' child1.getName() // TypeError: child1.getName is not a function // 缺点1:构造继承只能继承父类的实例属性和方法,不能继承父类原型的属性和方法 child1 instanceof Parent // fasle // 缺点2:实例并不是父类的实例,只是子类的实例
(3) 组合继承(原型链继承 + 构造继承)
// 使用原型链继承来保证子类能继承到父类原型中的属性和方法; // 使用构造继承来保证子类能继承到父类的实例属性和方法 function Parent (name) { // call()和new的时候会被调用两次产生无意义的内存开销, new后的实例中name值为undefined this.name = name } Parent.prototype.getName = function () { console.log(this.name) } function Child (name) { this.sex = 'boy' Parent.call(this, name) // 等同于this.name = name,使用call() 调用父类的属性(显式调用) } Child.prototype = new Parent() // 将Child.prototype 指向Parent的实例, 等同于Child.prototype.__proto__ == Parent.prototype Child.prototype.getSex = function () { console.log(this.sex) } var child1 = new Child('child1') child1 // {sex: 'boy', name: 'child1'} // child1原型链:Child {sex: 'boy', name: 'child1'}name : "child1"sex : "boy" [[Prototype]]:Parent getSex : ƒ ()name : undefined [[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object child1.getName() // child1 取的是Parent原型对象上的方法, 因为child1的原型对象指向了Parent实例 child1.getSex() // boy 取的是Child对象的原型对象Child的getSex方法 child1.constructor // f Parent (name) {this.name = name} // 正常来将child1.constructor指向构造函数Child本身,由于Child.prototype的继承Parent实例,导致Child.prototype.constructor被切断,沿着原型链找,最终指向了Parent.prototype的constructor, 也就是Parent (name) {this.name = name} // constructor 其实只是个标识作用,再实际的代码中并没有实际意义,所以是否在组合继承中修复这个地方取决于自己 Child.prototype.constrcutor = Child // 认亲爹 // =============================================================================================================== // 如何修改隐式原型(可以修改,但是在任何时候都不建议去人工修改隐式原型) // 经典调用案例 var a; (function () { function A () { this.a = 1 this.b = 2 } A.prototype.logA = function () { console.log(this.a) } a = new A() // 实例化A, 赋值给全局window的a })() a.logA() // 1 // 如何在匿名函数外给A这个构造函数的原型对象中添加一个方法logB用以打印出this.b a.constructor.prototype.logB = function() { console.log(this.b) } a.logB() // 2 a.constructor指向A构造函数本身 // 用隐式原型也可以, a的__proto__指向A.prototype a.__proto__.logB = function() { console.log(this.b) } // =============================================================================================================== function Parent (name, colors) { this.name = name this.colors = colors } Parent.prototype.features = ['cute'] function Child (name, colors) { Parent.apply(this, [name, colors]) } Child.prototype = new Parent() Child.prototype.constructor = Child var child1 = new Child('child1', ['white']) child1.colors.push('yellow') child1.features.push('sunshine') var child2 = new Child('child2', ['black']) child1.colors // ['cute', 'sunshine'] child2.colors // ['white', 'yellow'] child1.features // ['cute', 'sunshine'] child2.features // ['cute', 'sunshine'] // features是定义在父类构造函数原型对象中的,是比new Parent()还要更深一层的对象; // 它只能解决原型(匿名实例)中引用属性共享的问题。features是Parent.prototype上的属性,相当于是爷爷那一级 // 组合继承的优点 // 优点: // 1.可以继承父类实例属性和方法,也能够继承父类原型属性和方法 // 2.弥补了原型链继承中引用属性共享的问题(注意原型的原型的属性依旧会共享给实例) // 3.可传参,可复用 // 缺点:原型链继承和构造继承的时候都会调用一次 // 1.使用组合继承时,父类构造函数会被调用两次 // 2.并且生成了两个实例,子类实例中的属性和方法会覆盖子类原型(父类实例)上的属性和方法,所以增加了不必要的内存。
(4) 寄生组合继承
- 核心:解决多次调用父类构造函数问题
// 1.Object.create(proto, propertiesObject) 创建一个新对象,新对象的原型链将指向指定的原型对象的方法。 // 参数一:需要指定的新对象的原型对象,即新对象通过原型链继承了原型对象的属性和方法 // 参数二:可选参数,给新对象自身添加新属性以及描述器 // 2.Object.setPrototypeOf(obj, prototype) 用于设置一个对象的原型的方法 // 参数一:要设置原型的对象 // 参数二:该对象的新原型 // 标准的寄生组合继承 // 使用 Object.create() function Parent (name) { this.name = name } Parent.prototype.getName = function () { console.log(this.name) } function Child (name) { this.sex = 'boy' Parent.call(this, name) } // 与组合继承的区别 Child.prototype = Object.create(Parent.prototype) // 创建了一个空对象,并且这个对象的__proto__属性是指向Parent.prototype var child1 = new Child('child1') child1 // Child {sex: 'boy', name: 'child1'} child1.getName() // child1 child1.constructor // Parent (name) this.name = name 指向继承的Parent console.log(child1.__proto__) // 原型链:Parent {}[[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object // =============================================================================================================== // 使用 Object.setPrototypeOf() function Parent (name) { this.name = name } Parent.prototype.getName = function () { console.log(this.name) } function Child (name) { this.sex = 'boy' Parent.call(this, name) } // 与组合继承的区别 Object.setPrototypeOf(Child.prototype, Parent.prototype) // 将Child的原型设置为Parent的对象 var child2 = new Child('child2') child2 // Child {sex: 'boy', name: 'child1'} child2.getName() // child2 child2.constructor // Child (name) {this.sex = 'boy'Parent.call(this, name)} child2.__proto__ // 原型链:Parent {constructor: ƒ}constructor : ƒ Child(name) [[Prototype]]:Object getName : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object // 可以得出Object.setPrototypeOf()方法设置对象的原型更加完美一些,它修复了子类原型中constructor的指向问题 // =============================================================================================================== // function Parent(name){ this.name = name this.face = 'cry' this.colors = ['white', 'black'] } Parent.prototype.features = ['cute'] Parent.prototype.getFeatures = function (){ console.log(this.features) } function Child(name){ Parent.call(this, name) this.sex = 'boy' this.face = 'smile' } Child.prototype = Object.create(Parent.prototype) Child.prototype.constructor = Child var child1 = new Child('child1') child1.colors.push('yellow') var child2 = new Child('child2') child2.features = ['sunshine'] console.log(child1) // 原型链:Child {name: 'child1', face: 'smile', colors: Array(3), sex: 'boy'}colors : (3) ['white', 'black', 'yellow']face : "smile"name : "child1"sex : "boy" [[Prototype]]:Parent constructor : ƒ Child(name) [[Prototype]]:Object features : ['cute']getFeatures : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object console.log(child2) // 原型链:Child {name: 'child2', face: 'smile', colors: Array(2), sex: 'boy', features: Array(1)}colors : (2) ['white', 'black']face : "smile"features : ['sunshine']name : "child2"sex : "boy" [[Prototype]]:Parent constructor : ƒ Child(name) [[Prototype]]:Object features : ['cute']getFeatures : ƒ ()constructor : ƒ Parent(name) [[Prototype]]:Object // 寄生组合继承算是ES6之前一种比较完美的继承方式。 // 它避免了组合继承中调用两次父类构造函数,初始化两次实例属性的缺点 // 它拥有了原型链继承、构造继承和组合继承的所有继承方式的优点 // 只调用了一次父类构造函数,只创建了一份父类属性 // 子类可以用到父类原型链上的属性和方法 // 能够正常的使用instanceOf和isPrototypeOf方法 // Object.setPrototypeOf()方法会在运行时动态地改变对象的原型,这可能会对性能产生一些影响; // 在创建对象时就应该使用Object.create()来设置原型链,而不是后期动态地改变原型链。
JavaScript进阶知识汇总~(二):https://developer.aliyun.com/article/1415678