Day1 - 前端高频面试题之基础版javascript
浅拷贝的实现java
var obj = {
a: 1,
b: 2,
c: [3, 4, 5]
}
// 浅拷贝
// 一、经过for-in方式实现
function simpleCopy (obj) {
if (typeof obj != 'object') {
return false
}
let copyObj = {}
for(var i in obj) {
copyObj[i] = obj[i]
}
return copyObj
}
simpleCopy(obj) // {"a":1,"b":2,"c":[3,4,5]}
// 二、经过属性描述符
function simpleCopy2(obj) {
if (typeof obj != 'object') {
return
}
let copyObj = {}
Object.entries(obj).forEach(item => {
copyObj[item[0]] = item[1]
})
return copyObj
}
simpleCopy2(obj)
复制代码
深拷贝的简易版本es6
// 一、JSON
function deepCopy1 (obj) {
return JSON.parse(JSON.stringify(obj))
}
// 二、复制属性时,进行判断,若是是数组或者对象,则再次调用拷贝函数
function deepCopy2(obj, copyObj) {
if (typeof obj != 'object') {
return
}
var copyObj = copyObj || {}
for(var i in obj) {
// 过滤null
if (typeof obj[i] === 'object' && Object.prototype.toString.call(obj[i]) !== '[object Null]') {
copyObj[i] = Array.isArray(obj[i]) ? [] : {}
deepCopy2(obj[i], copyObj[i])
} else {
copyObj[i] = obj[i]
}
}
return copyObj;
}
deepCopy2(obj)
复制代码
推荐查看 lodash 的深拷贝函数面试
常见笔试题:数组
// 声明一个函数表达式
var add = function(x){
var sum = x;
// 在函数表达式内部有一个求和的内部函数
var tmp = function(y){
sum += y;// 求和
return tmp;
}
// 构建一个函数体的toString()函数
tmp.toString = function(){
return sum;
}
return tmp; // JavaScript中,打印和相加计算,会分别调用toString或valueOf函数,因此咱们重写tmp的toString和valueOf方法,返回sum的值
}
add(1)(2)(3)(4) // function 10
var a = add(1)(2)(3)(4).toString(); //10
// 一、函数add(1)第一次调用,实际上是只声明了var sum 这个变量,而后返回了tmp函数体,用于后面调用tmp函数
//二、函数add(1)(2)第二次调用才真正的把参数传进来使用了,即第一次传的 1 是没地方用的,没意义,第二次传的 2 是给第一次返回的tmp函数体传的参、即用在 sum=sum+x上 ---- sum=1+2
// 三、函数add(1)(2)(3)第三次调用和第二次同样,因为tmp函数体内部 return tmp 返回了自己,因此后面能够继续调用tmp函数,也就是除第一次调用传参无效外,后面能够调用无数次,sum值会不断累加
// 四、toString是tmp函数体附带的属性方法函数,会随着主体函数toString执行一次调用一次
复制代码
每一个函数在定义时都会自动带一个prototype属性,该属性是一个指针,指向一个对象,该对象称之为原型对象(经过prototype实现js的继承)。闭包
原型对象上默认有一个属性constructor,指向该原型对象对应的构造函数。app
经过调用构造函数建立的实例对象,都有一个内部属性__proto__,指向该构造函数的原型对象。其实例对象能够访问该原型对象上的全部属性和方法。函数
总结:post
每一个构造函数都有一个原型对象,原型对象上包含一个指向构造函数的指针,而实例对象都包含一个指向原型对象的内部指针。
通俗的说,实例对象经过内部指针__proto__访问到原型对象,原型对象经过constructor找到构造函数。
Foo.prototype只是一个指针,指向Foo的原型对象,利用这个指针能够实现JS的继承。
原型链的做用:
// 面试题1:
var F = function () {}
Object.prototype.a = function () {}
Function.prototype.b = function () {}
var f = new F()
// 请问f有方法a 方法b吗
f.a() // success
// because: f.__proto__ === F.prototype F.prototype.__proto__ === Object.prototype
f.b() // f.b is not a function
// 而f的原型链上没通过Function.prototype
复制代码
// 面试题2:
function Foo () {} // 构造函数
let foo1 = new Foo() // 实例对象
let foo2 = new Foo() // 实例对象
let obj = {}
// 写出 foo1 foo2 Foo Function obj Object的原型链 ???
foo1.__proto__ === Foo.prototype
foo2.__proto__ === Foo.prototype
Foo.__proto__ === Object.prototype
Foo.prototype === Object.prototype
Foo.prototype.__proto__ === Object.prototype
Foo.prototype.constructor === Foo
// Function.prototype是引擎创造出来的对象,一开始就有了,又由于其余的构造函数均可以经过原型链找到Function.prototype,Function自己也是一个构造函数,为了避免产生混乱,就将这两个联系到一块儿了
Function.__proto__ === Function.prototype
Function.prototype === Function.prototype
Function.prototype.__proto__ === Object.prototype
Function.prototype.constructor === Function
obj.__proto__ === Object.prototype
// Object是对象的构造函数,那么它也是一个函数,固然它的__proto__也是指向Function.prototype
Object.__proto__ === Function.prototype
Object.prototype === Object.prototype
Object.prototype.__proto__ === null
Object.prototype.constructor === Object
// 构造函数有一个prototype属性,指向实例对象的原型对象
Foo.prototype === foo1.__proto__
// 经过同一个构造函数实例化的多个对象具备相同的原型对象
foo1.__proto__ === foo2.__proto__
// 原型对象上默认有一个属性constructor,指向该原型对象对应的构造函数
Foo.prototype.constructor === Foo
foo1.__proto__.constructor === Foo
// 因为实例对象能够继承原型对象的属性,因此实例对象也拥有constructor属性,一样指向原型对象对应的构造函数
foo1.constructor === Foo
// isPrototypeOf用来判断实例对象与原型对象的关系
Foo.prototype.isPrototypeOf(f1) // true
// Object.getPrototypeOf() 返回该实例对象对应的原型对象,和proto是同样的,都返回原型对象
Object.getPrototypeOf(foo1) // Foo.prototype
Object.getPrototypeOf(foo1) === foo1.__proto__
// in操做符能够判断某个属性在不在对象上,但没法区分是自有属性仍是继承属性
// hasOwnPrototype能够判断该属性是自有属性仍是继承属性
复制代码
一、对象字面量方式
var person = {
name: 'Jack',
age: 18,
sayName: function () {
alert(this.name);
}
}
// 大量重复代码
复制代码
二、工厂模式
function a (name, age) {
var obj = new Object()
obj.name = name
obj.age = age
obj.alert = function () {
alert(this.name)
}
return obj
}
var a1 = a('name', 'age')
// 工厂模式就是批量化生产, 因为是工厂暗箱操做的,因此你不能识别这个对象究竟是什么类型
复制代码
三、构造函数(用来初始化新建立的对象的函数就是构造函数)
function Person (name, age) {
this.name = name
this.age = age
this.say = function () {
alert(this.name + this.age)
}
}
var p1 = new Person('xxx', 22)
var p1 = new Person('yyy', 18)
// 拥有相同的功能的两个实例,却分配了不一样的内存,浪费了内存空间
p1.say === p2.say // false
----------
// 构造函数拓展模式
// 把方法转移到构造函数外部,能够解决方法被重复建立的问题。
function Person (name, age) {
this.name = name
this.age = age
this.say = say
}
function say () {
alert(this.name + this.age)
}
var p1 = new Person('xxx', 22)
var p1 = new Person('yyy', 18)
p1.say === p2.say // true
// 可是这是在全局做用域中定义,并且只供一个对象调用,不符合全局做用域的定义规范;而且若是有多个方法时,就要多个全局函数,严重污染全局空间。
-------------
// 寄生构造函数模式(是工厂模式和构造函数模式的结合)
function A (name, age) {
var obj = new Object()
obj.name = name
obj.age = age
obj.alert = function () {
alert(this.name)
}
return obj
}
var a1 = new A('name', 'age')
var a2 = new A('name', 'age')
// 寄生构造函数模式与构造函数模式有相同的问题,每一个方法都要在每一个实例上从新建立一遍,建立多个完成相同任务的方法彻底没有必要,浪费内存空间
a1.alert === a2.alert // false
// 使用该模式返回的对象与构造函数之间没有关系。所以,使用instanceof运算符和prototype属性都没有意义
a1.__proto === A.prototype // false
复制代码
四、原型模式
function Person () {}
Person.prototype = {
constructor: Person, // 显示设置原型对象的constructor属性
name: 'xxx',
age: 20,
favoraties: [],
say: function () {
alert(this.name + this.age)
}
}
var p1 = new Person()
var p2 = new Person()
p1.favoraties.push('sing')
p2.favoraties.push('song')
p1.favoraties // ["sing", "song"]
p2.favoraties // ["sing", "song"]
// 引用类型的值在原型对象上会被共享,修改一个实例的值,也会改变其余实例的变化
复制代码
五、组合使用构造模式和原型模式
function Person (name, age) {
this.name = name
this.age = age
this.say = function () {
alert(this.age)
}
}
Person.prototype = {
constructor: Person,
totalHobby: 'running',
sing: function () {
alert(this.name)
}
}
var p1 = new Person('xxx', 20)
var p1 = new Person('yyy', 18)
// 将独立的属性和方法放在构造函数中,须要共享的属性和方法放在原型对象中,还支持向构造函数传递参数,这样能够最大限度的节省内存而又保留实例对象的独立性
----------
// 动态原型模式
// 动态原型模式将组合模式中分开使用的构造函数和原型对象都封装到了构造函数中,而后经过检查方法是否被建立,来决定是否初始化原型对象,也减小了全局空间的污染
function Person (name, age) {
this.name = name
this.age = age
if (typeof this.say != 'function') {
this.prototype.say = function () {
alert(this.age)
}
}
}
var p1 = new Person('name', 'age')
p1.say()
复制代码
一、原型继承
// 原型继承 (本质就是重写原型对象,代之以一个新类型的实例)
function Super () {
this.value = true
this.colors = ['red']
}
Super.prototype.getValue = function () {
return this.value
}
function Sub () {}
Sub.prototype = new Super() // here
Sub.prototype.constructor = Sub
var instance = new Sub()
instance.getValue() // true
var instance2 = new Sub()
instance.colors.push('green')
instance.colors // ["red", "green"]
instance2.colors // ["red", "green"]
// 第一个缺点是全部子类共享父类的实例,其中一个子类修改了父类中引用对象的值,其余子类的属性值也会被修改
// 第二个缺点是在构造子类实例的时候,不能给父类传递参数。实际上,是没有办法在不影响全部对象实例的状况下,给父类传递参数。
复制代码
二、构造函数实现继承
// 构造函数继承,也叫作 伪类继承 或 经典继承 (本质就是在子类构造函数内部借用call/apply方法调用父类的构造函数)
function Super (name, colors) {
this.name = name
this.colors = colors
}
function Sub () {
// 继承了Super
Super.call(this, 'xyz', ['red'])
}
var instance1 = new Sub()
instance1.name // xyz
instance1.colors.push('white')
instance1.colors // ['red', 'white']
var instance2 = new Sub()
instance2.colors // ['red']
// 相对于原型继承来讲,有一个很大的优点就是能够传递参数给父类,而且也能够解决引用类型实例属性共享的问题。
// 缺点是方法都定义在构造函数内部,没法复用
复制代码
三、组合式继承
// 组合继承(原型继承+构造函数继承)
function Super (name) {
this.name = name
this.colors = ['red']
}
Super.prototype.getName = function () {
return this.name
}
function Sub (name, age) {
// 继承属性
// 第二次调用父类构造函数,Sub.prototype获得了name和colors,并覆盖了第一次获得的属性
Super.call(this, name)
this.age = age
}
// 继承方法
// 第一次调用父类构造函数,Sub.prototype获得了内部属性name,colors
Sub.prototype = new Super('xyz')
Sub.prototype.constructor = Sub
// 子类本身的方法
Sub.prototype.getAge = function () {
return this.age
}
var instance1 = new Sub('xyz', 22)
instance1.getName() // xyz
instance1.getAge() // 22
instance1.colors.push('white')
instance1.colors // ['red', 'white']
var instance2 = new Sub('zzz', 18)
instance2.colors // ['red']
// 缺点是不管什么状况下,总会调用两次父类构造函数。第一次是在建立子类原型的时候,第二次是在子类构造函数内部
// 占用的空间更大了
复制代码
四、寄生组合式继承
// 寄生组合式继承,解决了调用两次父类构造函数的问题
// 也是借用构造函数来继承不可共享的属性,经过原型链的混成形式来继承方法和可共享的属性。只不过把原型继承改为了寄生式继承。
// 使用寄生组合继承能够没必要为了指定子类的原型而调用父类的构造函数,因此只继承了父类的原型属性,而父类的实例属性是借用构造函数的方式来获得的。
function Super (name) {
this.name = name
this.colors = ['red']
}
Super.prototype.getName = function () {
return this.name
}
function Sub (name, age) {
Super.call(this, name)
this.age = age
}
// 本质上,是对传入的对象进行了一次浅复制
if (!Object.create){
Object.create = function (proto) {
function F() {} // 临时的构造函数
F.prototype = proto // 将传进来的参数做为这个构造函数的原型
return new F() // 返回这个构造函数的新实例
}
}
Sub.prototype = Object.create(Super.prototype)
Sub.prototype.constructor = Sub
var instance1 = new Sub('xyz', 18)
instance1.getName() // xyz
instance1.colors.push('white')
instance1.colors // ['red', 'white']
var instance2 = new Sub('xyz2', 22)
instance2.colors // ['red']
// 高效率体如今只调用了一次Super构造函数,而且所以避免了在Sub.prototype上建立没必要要的、多余的属性。并且原型链不变
复制代码
ES5的继承 是经过原型或构造函数机制来实现,实质上是先建立子类的实例对象,而后再将父类的方法添加到this上(Parent.apply(this))。
五、es6中的class ES6封装了class,extends关键字来实现继承,实质上是先建立父类的实例对象this(因此必须先调用父类的super()方法),而后再用子类的构造函数修改this。
// class
class Super {
constructor (name) {
this.name = name
this.colors = ['red']
}
getName () {
return this.name
}
}
class Sub extends Super {
constructor (name, age) {
super(name)
this.age = age
}
getAge () {
return this.age
}
}
var instance1 = new Sub('xyz', 18)
instance1.colors.push('white')
instance1.colors // ['red', 'white']
instance1.getName() // xyz
var instance2 = new Sub('xyz2', 22)
instance2.colors // ['red']
instance2.getAge() // 22
复制代码