2020年前端面试题集锦(上)

167 阅读35分钟

基础知识点与高频考题 JavaScript基础

console.log(1 < 2 < 3);
console.log(3 > 2 > 1);
// 写出代码执行结果,并解释为什么

// 答案与解析
true false
对于运算符>、<,一般的计算从左向右
第一个题:1 < 2 等于 true, 然后true < 3,true == 1 ,因此结果是true
第二个题:3 > 2 等于 true, 然后true > 1, true == 1 ,因此结果是false

/*********************************/

[typeof null, null instanceof Object]
// 写出代码执行的结果,并解释为什么

//答案与解析
 ["object", false]
1)typeof操作符返回一个字符串,表示未经计算的操作数的类型
	类型					结果
Undefined		"undefined"
Null				"object"
Boolean			"boolean"
Number			"number"
String			"string"
Symbol			"symbol"
函数对象			"function"
任何其他对象	"object"
typeof null === 'object';// 从最开始的时候javascript就是这样
JavaScript 中的值是由一个表示类型的标签和实际数据值表示的。对象的类型标签是 0。由于 null 代表的是空指针(大多数平台下值为 0x00),因此,null的类型标签也成为了 0,typeof null就错误的返回了"object"。这算一个bug,但是被拒绝修复,因为影响的web系统太多

2)instanceof 运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype 属性
null不是以Object原型创建,因此返回false

/*********************************/
// 死循环陷阱
var END = Math.pow(2, 53);
var START = END - 100;
var count = 0;
for (var i = START; i <= END; i++) {
    count++;
}
console.log(count);
// 写出正确的打印结果,并解释为什么?

// 答案与解析
这个会陷入一个死循环,2^53是最大的值,因此2^53+1 等于 2^53

/*********************************/

var a = Date(0);
var b = new Date(0);
var c = new Date();
[a === b, b === c, a === c];
// 写出代码执行结果

// 答案与解析
 [false,false,false]
需要注意的是只能通过调用 Date 构造函数来实例化日期对象:以常规函数调用它(即不加 new 操作符)将会返回一个字符串,而不是一个日期对象。另外,不像其他JavaScript 类型,Date 对象没有字面量格式。
a是字符串,b和c是Date对象,并且b代表的是1970年那个初始化时间,而c代表的是当前时间。

/*********************************/
// 逗号表达式
var x = 20;
var temp = {
    x: 40,
    foo: function() {
        var x = 10;
      	console.log(this.x);
    }
};
(temp.foo, temp.foo)();

// 写出打印结果
20
逗号操作符,逗号操作符会从左到右计算它的操作数,返回最后一个操作数的值。所以(temp.foo, temp.foo)();等价于var fun = temp.foo; fun();,fun调用时this指向window,所以返回20。

/*********************************/
链式调用
// 实现 (5).add(3).minus(2) 功能
// console.log((5).add(3).minus(2)); // 6

/*
	答案:12
	arguments中c的值还是1不会变成10,  
	因为a函数加了默认值,就按ES的方式解析,ES6是有块级作用域的,所以c的值是不会改变的
*/
Number.prototype.add = function (number) {
    if (typeof number !== 'number') {
        throw new Error('请输入数字~');
    }
    return this + number;
};
Number.prototype.minus = function (number) {
    if (typeof number !== 'number') {
        throw new Error('请输入数字~');
    }
    return this - number;
};
console.log((5).add(3).minus(2));

/*********************************/

var a = 1;        
(function a () {            
    a = 2;            
    console.log(a);        
})();

// 答案
ƒ a () {            
    a = 2;            
    console.log(a);        
}
/*
立即调用的函数表达式(IIFE) 有一个 自己独立的 作用域,如果函数名称与内部变量名称冲突,就会永远执行函数本身;所以上面的结果输出是函数本身;
*/

/*********************************/

var a = [0];
if(a){
    console.log(a == true);
}else{
    console.log(a);
}

/*
答案:false
当a出现在if的条件中时,被转成布尔值,而Boolean([0])为true,所以就进行下一步判断 a == true,在进行比较时,[0]被转换成了0,所以0==truefalse
js的规则是:  
如果比较的是原始类型的值,原始类型的值会转成数值再进行比较
1 == true  //true   1 === Number(true)
'true' == true //false Number('true')->NaN  Number(true)->1
'' == 0//true
'1' == true//true  Number('1')->1
对象与原始类型值比较,对象会转换成原始类型的值再进行比较。
undefined和null与其它类型进行比较时,结果都为false,他们相互比较时结果为true。



下边的不要了sadfasdfasdfasdfsadfasdffasdfasdf
一些隐式转换
数组从非primitive转为primitive的时候会先隐式调用join变成“0”,string和boolean比较的时候,两个都先转为number类型再比较,最后就是0==1的比较了
var a = [1];
if(a){
    console.log(a == true);
}else{
    console.log(a);
}
// true
!![] //true  空数组转换为布尔值是true,  
!![0]//true  数组转换为布尔值是true  
[0] == true;//false   数组与布尔值比较时却变成了false  
Number([])//0  
Number(false)//0  
Number(['1'])//1

所以当a出现在if的条件中时,被转成布尔值,而Boolean([0])为true,所以就进行下一步判断 a == true,在进行比较时,js的规则是:  
如果比较的是原始类型的值,原始类型的值会转成数值再进行比较
1 == true  //true   1 === Number(true)
'true' == true //false Number('true')->NaN  Number(true)->1
'' == 0//true
'1' == true//true  Number('1')->1
对象与原始类型值比较,对象会转换成原始类型的值再进行比较。
undefined和null与其它类型进行比较时,结果都为false,他们相互比较时结果为true。
*/

/****************************/

const a = [1,2,3],
    b = [1,2,3],
    c = [1,2,4],
		d = "2",
		e = "11";
console.log([a == b, a === b, a > c, a < c, d > e]);
// 写出正确打印结果

// 答案
[false,false,false,true,true] 

// 解析
1)JavaScript 有两种比较方式:严格比较运算符和转换类型比较运算符。
	对于严格比较运算符(===)来说,仅当两个操作数的类型相同且值相等为 true,而对于被广泛使用的比较运算符(==)来说,会在进行比较之前,将两个操作数转换成相同的类型。对于关系运算符(比如 <=)来说,会先将操作数转为原始值,使它们类型相同,再进行比较运算。
	当两个操作数都是对象时,JavaScript会比较其内部引用,当且仅当他们的引用指向内存中的相同对象(区域)时才相等,即他们在栈内存中的引用地址相同。
	javascript中Array也是对象,所以这里a,b,c显然引用是不相同的,所以这里a==b,a===b都为false。

2)两个数组进行大小比较,也就是两个对象进行比较
	当两个对象进行比较时,会转为原始类型的值,再进行比较。对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法。
①valueOf() 方法返回指定对象的原始值。
  JavaScript调用valueOf方法将对象转换为原始值。你很少需要自己调用valueOf方法;当遇到要预期的原始值的对象时,JavaScript会自动调用它。默认情况下,valueOf方法由Object后面的每个对象继承。 每个内置的核心对象都会覆盖此方法以返回适当的值。如果对象没有原始值,则valueOf将返回对象本身。
②toString() 方法返回一个表示该对象的字符串。
  每个对象都有一个 toString() 方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString() 方法被每个 Object 对象继承。如果此方法在自定义对象中未被覆盖,toString() 返回 "[object type]",其中 type 是对象的类型。
③经过valueOf,toString的处理,所以这里a,c最终会被转换为"1,2,3""1,2,4";

3)两个字符串进行比较大小
	上边的数组经转换为字符串之后,接着进行大小比较。
	MDN中的描述是这样的:字符串比较则是使用基于标准字典的 Unicode 值来进行比较的。
	字符串按照字典顺序进行比较。JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。
	所以这里 "1,2,3" < "1,2,4",输出true,因为前边的字符的unicode码点都相等,所以最后是比较3和4的unicode码点。而3的Unicode码点是51,4的uniCode码点是52,所以a<c。
"2" > "11"也是同理,这个也是开发中有时会遇到的问题,所以在进行运算比较时需要注意一下。

4)关于valueOf,toString的调用顺序
①javascript中对象到字符串的转换经历的过程如下:
	如果对象具有toString()方法,javaScript会优先调用此方法。如果返回的是一个原始值(原始值包括null、undefined、布尔值、字符串、数字),javaScript会将这个原始值转换为字符串,并返回字符串作为结果。
	如果对象不具有toString()方法,或者调用toString()方法返回的不是原始值,则javaScript会判断是否存在valueOf()方法,如若存在则调用此方法,如果返回的是原始值,javaScript会将原始值转换为字符串作为结果。
  如果javaScript无法调用toString()和valueOf()返回原始值的时候,则会报一个类型错误异常的警告。
	比如:String([1,2,3]);将一个对象转换为字符串
	var a = [1,2,3];
	a.valueOf = function(){
    console.log("valueOf");
    return this
  }
	a.toString = function(){
    console.log('toString')
    return this
  }	
	String(a);
	因为这里我返回的是this,最后,所以如果javaScript无法调用toString()和valueOf()返回原始值的时候,则会报一个类型错误异常的警告。
  
②javaScript中对象转换为数字的转换过程:
	javaScript优先判断对象是否具有valueOf()方法,如具有则调用,若返回一个原始值,javaScript会将原始值转换为数字并作为结果。
	如果对象不具有valueOf()方法,javaScript则会调用toString()的方法,若返回的是原始值,javaScript会将原始值转换为数字并作为结果。
	如果javaScript无法调用toString()和valueOf()返回原始值的时候,则会报一个类型错误异常的警告。
	比如:Number([1,2,3]);将一个对象转换为字符串
  
/*****************************************/
  
var a = ?;
if(a == 1 && a== 2 && a== 3){
 	console.log(1);
}

/*
比较操作涉及多不同类型的值时候,会涉及到很多隐式转换,其中规则繁多即便是经验老道的程序员也没办法完全记住,特别是用到 `==` 和 `!=` 运算时候。所以一些团队规定禁用 `==` 运算符换用`===` 严格相等。
*/
// 答案一
var aᅠ = 1;
var a = 2;
var ᅠa = 3;
if(aᅠ==1 && a== 2 &&ᅠa==3) {
    console.log("1")
}
/*
	考察你的找茬能力,注意if里面的空格,它是一个Unicode空格字符,不被ECMA脚本解释为空格字符(这意味着它是标识符的有效字符)。所以它可以解释为
  var a_ = 1;
  var a = 2;
  var _a = 3;
  if(a_==1 && a== 2 &&_a==3) {
      console.log("1")
  }
*/
//答案二
var a = {
  i: 1,
  toString: function () {
    return a.i++;
  }
}
if(a == 1 && a == 2 && a == 3) {
  console.log('1');
}
/*
	如果原始类型的值和对象比较,对象会转为原始类型的值,再进行比较。
	对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法。
*/
// 答案三
var a = [1,2,3];
a.join = a.shift;
console.log(a == 1 && a == 2 && a == 3);
/*
	比较巧妙的方式,array也属于对象,
	对于数组对象,toString 方法返回一个字符串,该字符串由数组中的每个元素的 toString() 返回值经调用 join() 方法连接(由逗号隔开)组成。
	数组 toString 会调用本身的 join 方法,这里把自己的join方法该写为shift,每次返回第一个元素,而且原数组删除第一个值,正好可以使判断成立
*/
// 答案四
var i = 0;
with({
  get a() {
    return ++i;
  }
}) {
  if (a == 1 && a == 2 && a == 3)
    console.log("1");
}
/*
	with 也是被严重建议不使用的对象,这里也是利用它的特性在代码块里面利用对象的 get 方法动态返回 i.
*/
// 答案五
var val = 0;
Object.defineProperty(window, 'a', {
  get: function() {
    return ++val;
  }
});
if (a == 1 && a == 2 && a == 3) {
  console.log('1');
}
/*
	全局变量也相当于 window 对象上的一个属性,这里用defineProperty 定义了 a的 get 也使得其动态返回值。和with 有一些类似。
*/

// 答案六
let a = {[Symbol.toPrimitive]: ((i) => () => ++i) (0)};
if (a == 1 && a == 2 && a == 3) {
  console.log('1');
}
/*
	ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。我们之前在定义类的内部私有属性时候习惯用 __xxx ,这种命名方式避免别人定义相同的属性名覆盖原来的属性,有了 Symbol  之后我们完全可以用 Symbol值来代替这种方法,而且完全不用担心被覆盖。
	除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。Symbol.toPrimitive就是其中一个,它指向一个方法,表示该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。这里就是改变这个属性,把它的值改为一个 闭包 返回的函数。
*/

业务中一般不会写出这种代码,重点还是知识点的考察

/*******************************************/

let a = {n: 1};
let b = a;
a.x = a = {n: 2};
console.log(a.x) 	
console.log(b.x)

答案:
undefined {n:2}

注意点:

1: 点的优先级大于等号的优先级
2: 对象以指针的形式进行存储,每个新对象都是一份新的存储地址

解析:

- `var b = a;` b 和 a 都指向同一个地址。
- `.`的优先级高于`=`。所以先执行`a.x`,于是现在的`a`和`b`都是`{n: 1, x: undefined}`。
- `=`是从右向左执行。所以是执行 `a = {n: 2}`,于是`a`指向了`{n: 2}`
- 再执行 `a.x = a`。 这里注意,`a.x` 是最开始执行的,已经是`{n: 1, x: undefined}`这个地址了,而不是一开的的那个`a`,所以也就不是`{n: 2}`了。而且`b`和旧的`a`是指向一个地址的,所以`b`也改变了。
- 但是,`=`右面的a,是已经指向了新地址的新`a`。
- 所以,`a.x = a` 可以看成是`{n: 1, x: undefined}.x = {n: 2}`
- 最终得出
  a = { n: 2 },
  b = {
     n: 1,
     x: { n: 2 }
  }

/*****************************************/

// 实现一个模板引擎
let template = '我是{{name}},年龄{{age}},性别{{sex}}';
let data = {
  name: '姓名',
  age: 18
}
render(template, data); // 我是姓名,年龄18,性别undefined
function render(template,data){
  // your code
}
// 补充代码,使代码可以正确执行

// 代码实现
function render(template, data) {
  const reg = /\{\{(\w+)\}\}/; // 模板字符串正则
  if (reg.test(template)) { // 判断模板里是否有模板字符串
    const name = reg.exec(template)[1]; // 查找当前模板里第一个模板字符串的字段
    template = template.replace(reg, data[name]); // 将第一个模板字符串渲染
    return render(template, data); // 递归的渲染并返回渲染后的结构
  }
  return template; // 如果模板没有模板字符串直接返回
}

/*****************************************/

什么是包装对象(wrapper object)?

// 答案
1)复习一下JS的数据类型,JS数据类型被分为两大类,基本类型和引用类型
①基本类型:Undefined,Null,Boolean,Number,String,Symbol,BigInt
②引用类型:Object,Array,Date,RegExp等,说白了就是对象。

2)其中引用类型有方法和属性,但是基本类型是没有的,但我们经常会看到下面的代码
let name = "marko";
console.log(typeof name); // "string"
console.log(name.toUpperCase()); // "MARKO"
name类型是 string,属于基本类型,所以它没有属性和方法,但是在这个例子中,我们调用了一个toUpperCase()方法,它不会抛出错误,还返回了对象的变量值。
原因是基本类型的值被临时转换或强制转换为对象,因此name变量的行为类似于对象。 除null和undefined之外的每个基本类型都有自己包装对象。也就是:String,Number,Boolean,Symbol和BigInt。 在这种情况下,name.toUpperCase()在幕后看起来如下:
console.log(new String(name).toUpperCase()); // "MARKO"
在完成访问属性或调用方法之后,新创建的对象将立即被丢弃。

防抖/节流

const debounce = (fn,delay) => {
  // 介绍防抖函数原理,并实现
  // your code
}
const throttle = (fn,delay = 500) => {
  // 介绍节流函数原理,并实现
   // your code
}

// 答案与解析
/*
	1)防抖函数原理:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
	适用场景:
		①按钮提交场景:防止多次提交按钮,只执行最后提交的一次
		②服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
*/
// 手写简化版实现
const debounce = (fn,delay) => {
  let timer = null;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn.apply(this.args);
    },delay)
  }
}

/*
	2)节流函数原理:规定在一个单位时间内,只能触发一次函数。如果这个单位时间内触发多次函数,只有一次生效。防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次
	适用场景:
		①拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
		②缩放场景:监控浏览器resize
		③动画场景:避免短时间内多次触发动画引起性能问题
*/
// 手写简化版实现
// ①定时器实现
const throttle = (fn,delay = 500) =>{
  let flag = true;
  return (...args) => {
    if(!flag) return;
    flag = false;
    setTimeout(() => {
      fn.apply(this,args);
      flag = true;
    },delay);
  };
}
// ②时间戳实现
const throttle = (fn,delay = 500) => {
  let preTime = Date.now();
  return (...args) => {
    const nowTime = Date.now();
    if(nowTime - preTime >= delay){
      	preTime = Date.now();
      	fn.apply(this,args);
    }
  }
}

new 的实现原理,模拟实现一下

// 首先,new操作符做了什么
new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。new 关键字会进行如下的操作
创建一个空的简单JavaScript对象(即{});
链接该对象(即设置该对象的构造函数)到另一个对象 ;
将步骤1新创建的对象作为this的上下文 ;
如果该函数没有返回对象,则返回this。即创建的这个的新对象,否则,返回构造函数中返回的对象。

// 参考答案:1.简单实现
function newOperator(ctor) {
    if (typeof ctor !== 'function'){
        throw 'newOperator function the first param must be a function';
    }
  	// ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
    var args = Array.prototype.slice.call(arguments, 1);
    // 1.创建一个空的简单JavaScript对象(即{})
    var obj = {};
    // 2.链接该新创建的对象(即设置该对象的__proto__)到该函数的原型对象prototype上
    obj.__proto__ = ctor.prototype;
    // 3.将步骤1新创建的对象作为this的上下文
    var result = ctor.apply(obj, args);
    // 4.如果该函数没有返回对象,则返回新创建的对象

    var isObject = typeof result === 'object' && result !== null;
    var isFunction = typeof result === 'function';
    return isObject || isFunction ? result : obj;
}

// 测试
function company(name, address) {
    this.name = name;
    this.address = address;
  }
  
var company1 = newOperator(company, 'yideng', 'beijing');
console.log('company1: ', company1);

// 参考答案:2.更完整的实现
/**
 * 模拟实现 new 操作符
 * @param  {Function} ctor [构造函数]
 * @return {Object|Function|Regex|Date|Error}      [返回结果]
 */
function newOperator(ctor){
    if(typeof ctor !== 'function'){
      throw 'newOperator function the first param must be a function';
    }
    // ES6 new.target 是指向构造函数
    newOperator.target = ctor;
    // 1.创建一个全新的对象,
    // 2.并且执行[[Prototype]]链接
    // 4.通过`new`创建的每个对象将最终被`[[Prototype]]`链接到这个函数的`prototype`对象上。
    var newObj = Object.create(ctor.prototype);
    // ES5 arguments转成数组 当然也可以用ES6 [...arguments], Aarry.from(arguments);
    // 除去ctor构造函数的其余参数
    var argsArr = [].slice.call(arguments, 1);
    // 3.生成的新对象会绑定到函数调用的`this`。
    // 获取到ctor函数返回结果
    var ctorReturnResult = ctor.apply(newObj, argsArr);
    // 小结4 这些类型中合并起来只有Object和Function两种类型 typeof null 也是'object'所以要不等于null,排除null
    var isObject = typeof ctorReturnResult === 'object' && ctorReturnResult !== null;
    var isFunction = typeof ctorReturnResult === 'function';
    if(isObject || isFunction){
        return ctorReturnResult;
    }
    // 5.如果函数没有返回对象类型`Object`(包含`Functoin`, `Array`, `Date`, `RegExg`, `Error`),那么`new`表达式中的函数调用会自动返回这个新的对象。
    return newObj;
}

this指向

// 来一道面试题
var a=10;
var foo={
  a:20,
  bar:function(){
      var a=30;
      return this.a;
    }
}
console.log(foo.bar());
console.log((foo.bar)());
console.log((foo.bar=foo.bar)());
console.log((foo.bar,foo.bar)());

// 答案:
20 20 10 10

// 第一问  foo.bar()
/*
	foo调用,this指向foo , 此时的 this 指的是foo,输出20
*/
// 第二问  (foo.bar)()
/*
	给表达式加了括号,而括号的作用是改变表达式的运算顺序,而在这里加与不加括号并无影响;相当于foo.bar(),输出20
*/
// 第三问  (foo.bar=foo.bar)()
/*
	等号运算,
	相当于重新给foo.bar定义,即
	foo.bar = function () {
    var a = 10;
    return this.a;
	}
	就是普通的复制,一个匿名函数赋值给一个全局变量
	所以这个时候foo.bar是在window作用域下而不是foo = {}这一块级作用域,所以这里的this指代的是window,输出10
*/
// 第四问  (foo.bar,foo.bar)()
/*
	1.逗号运算符,
	2.逗号表达式,求解过程是:先计算表达式1的值,再计算表达式2的值,……一直计算到表达式n的值。最后整个逗号表达式的值是表达式n的值。逗号运算符的返回值是最后一个表达式的值。
  3.其实这里主要还是经过逗号运算符后,就是纯粹的函数了,不是对象方法的引用,所以这里this指向的是window,输出10
  4.第三问,第四问,一个是等号运算,一个是逗号运算,可以这么理解,经过赋值,运算符运算后,都是纯粹的函数,不是对象方法的引用。所以函数指向的this都是windows的。
*/

如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

首先来看一下this绑定的规则,来详细看一下,这样再遇到this的问题,可以从容应对

默认绑定

// 默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。
function sayHi(){
    console.log('Hello,', this.name);
}
var name = 'yideng';
sayHi();
//在调用Hi()时,应用了默认绑定,this指向全局对象(非严格模式下),严格模式下,this指向undefined,undefined上没有this对象,会抛出错误。
// 如果在浏览器环境中运行,那么结果就是 Hello,yideng
// 如果在node环境中运行,结果就是 Hello,undefined.这是因为node中name并不是挂在全局对象上的。

隐式绑定

// 函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun()
function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'yidneg1',
    sayHi: sayHi
}
var name = 'yidneg2';
person.sayHi();
// Hello yideng1
// sayHi函数声明在外部,严格来说并不属于person,但是在调用sayHi时,调用位置会使用person的上下文来引用函数,隐式绑定会把函数调用中的this(即此例sayHi函数中的this)绑定到这个上下文对象(即此例中的person)

需要注意的是:对象属性链中只有最后一层会影响到调用位置。

function sayHi(){
    console.log('Hello,', this.name);
}
var person2 = {
    name: 'yideng1',
    sayHi: sayHi
}
var person1 = {
    name: 'yideng2',
    friend: person2
}
person1.friend.sayHi();
// Hello yideng1
//因为只有最后一层会确定this指向的是什么,不管有多少层,在判断this的时候,我们只关注最后一层,即此处的friend。

**隐式绑定有一个大陷阱,**绑定很容易丢失(或者说容易给我们造成误导,我们以为this指向的是什么,但是实际上并非如此).

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'yideng1',
    sayHi: sayHi
}
var name = 'yideng2';
var Hi = person.sayHi;
Hi();
// Htllo yideng2
// Hi直接指向了sayHi的引用,在调用的时候,跟person没有半毛钱的关系,针对此类问题,我建议大家只需牢牢记住这个格式:XXX.fn();fn()前如果什么都没有,那么肯定不是隐式绑定。

除了上面这种丢失之外,隐式绑定的丢失是发生在回调函数中(事件回调也是其中一种),

function sayHi(){
    console.log('Hello,', this.name);
}
var person1 = {
    name: 'yideng1',
    sayHi: function(){
        setTimeout(function(){
            console.log('Hello,',this.name);
        })
    }
}
var person2 = {
    name: 'yideng2',
    sayHi: sayHi
}
var name='yideng3';
person1.sayHi();
setTimeout(person2.sayHi,100);
setTimeout(function(){
    person2.sayHi();
},200);

// Hello yideng3
// Hello yideng3
// Hello yideng2

1.第一条输出很容易理解,setTimeout的回调函数中,this使用的是默认绑定,非严格模式下,执行的是全局对象
2.第二条输出是不是有点迷惑了?说好XXX.fun()的时候,fun中的this指向的是XXX呢,为什么这次却不是这样了!Why?
其实这里我们可以这样理解: setTimeout(fn,delay){ fn(); },相当于是将person2.sayHi赋值给了一个变量,最后执行了变量,这个时候,sayHi中的this显然和person2就没有关系了。
3.第三条虽然也是在setTimeout的回调中,但是我们可以看出,这是执行的是person2.sayHi()使用的是隐式绑定,因此这是this指向的是person2,跟当前的作用域没有任何关系。

看到这里是不是有点疲倦了

显示绑定

显式绑定比较好理解,就是通过call,apply,bind的方式,显式的指定this所指向的对象。

call,apply和bind的第一个参数,就是对应函数的this所指向的对象。call和apply的作用一样,只是传参方式不同。call和apply都会执行对应的函数,而bind方法不会。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'yideng1',
    sayHi: sayHi
}
var name = 'yideng2';
var Hi = person.sayHi;
Hi.call(person); //Hi.apply(person)
// Hello yideng1  因为使用硬绑定明确将this绑定在了person上。

使用了硬绑定,是不是意味着不会出现隐式绑定所遇到的绑定丢失呢?显然不是这样的,不信,继续往下看。

function sayHi(){
    console.log('Hello,', this.name);
}
var person = {
    name: 'yideng1',
    sayHi: sayHi
}
var name = 'yideng2';
var Hi = function(fn) {
    fn();
}
Hi.call(person, person.sayHi); 
// Hello yideng2
输出的结果是 Hello, Wiliam. 原因很简单,Hi.call(person, person.sayHi)的确是将this绑定到Hi中的this了。但是在执行fn的时候,相当于直接调用了sayHi方法(记住: person.sayHi已经被赋值给fn了,隐式绑定也丢了),没有指定this的值,对应的是默认绑定。
现在,我们希望绑定不会丢失,要怎么做?很简单,调用fn的时候,也给它硬绑定。

var Hi = function(fn) {
    fn.call(this);
}
这样就行了
因为person被绑定到Hi函数中的this上,fn又将这个对象绑定给了sayHi的函数。这时,sayHi中的this指向的就是person对象。

new 绑定

javaScript和C++不一样,并没有类,在javaScript中,构造函数只是使用new操作符时被调用的函数,这些函数和普通的函数并没有什么不同,它不属于某个类,也不可能实例化出一个类。任何一个函数都可以使用new来调用,因此其实并不存在构造函数,而只有对于函数的“构造调用”

前边我们提到new 操作符都干了什么

创建一个空对象,构造函数中的this指向这个空对象 这个新对象被执行 [[原型]] 连接 执行构造函数方法,属性和方法被添加到this引用的对象中 如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。 因此,我们使用new来调用函数的时候,就会新对象绑定到这个函数的this上。

function sayHi(name){
    this.name = name;
	
}
var Hi = new sayHi('yideng');
console.log('Hello,', Hi.name); // Hello yideng

绑定优先级

我们知道了this有四种绑定规则,但是如果同时应用了多种规则,怎么办? 显然,我们需要了解哪一种绑定方式的优先级更高,这四种绑定的优先级为: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定 感兴趣的可以写个demo测试看看 绑定例外

凡事都有例外,this的规则也是这样。

如果我们将null或者是undefined作为this的绑定对象传入call、apply或者是bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。

var foo = {
    name: 'yideng1'
}
var name = 'yideng2';
function bar() {
    console.log(this.name);
}
bar.call(null); //yideng2
因为这时实际应用的是默认绑定规则。

箭头函数

箭头函数是ES6中新增的,它和普通函数有一些区别,箭头函数没有自己的this,它的this继承于外层代码库中的this。箭头函数在使用时,需要注意以下几点: 函数体内的this对象,继承的是外层代码块的this。 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。 不可以使用yield命令,因此箭头函数不能用作 Generator 函数。 箭头函数没有自己的this,所以不能用call()、apply()、bind()这些方法去改变this的指向.

一道综合面试题

function Foo() {
    getName = function () { console.log(1); };
    return this;
}
Foo.getName = function () { console.log(2);};
Foo.prototype.getName = function () { console.log(3);};
var getName = function () { console.log (4);};
function getName() { console.log (5);}
 
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();

// 2 4 1 1 2 3 3

这道题的经典之处在于它综合考察了面试者的JavaScript的综合能力,包含了变量定义提升、this指针指向、运算符优先级、原型、继承、全局变量污染、对象属性及原型属性优先级等知识

先看此题的上半部分做了什么,

首先定义了一个叫Foo的函数, 之后为Foo创建了一个叫getName的静态属性存储了一个匿名函数, 之后为Foo的原型对象新创建了一个叫getName的匿名函数。 之后又通过函数变量表达式创建了一个getName的函数, 最后再声明一个叫getName函数。 第一问:Foo.getName 自然是访问Foo函数上存储的静态属性,答案自然是2,一般来说第一问对于稍微懂JS基础的同学来说应该是没问题的

// 回顾下基础知识
function User(name) {
	var name = name; //私有属性
	this.name = name; //公有属性
	function getName() { //私有方法
		return name;
	}
}
User.prototype.getName = function() { //公有方法
	return this.name;
}
User.name = 'yideng'; //静态属性
User.getName = function() { //静态方法
	return this.name;
}
var yideng = new User('yideng'); //实例化

几个注意点:

调用公有方法,公有属性,我们必需先实例化对象,也就是用new操作符实化对象,就可构造函数实例化对象的方法和属性,并且公有方法是不能调用私有方法和静态方法的 静态方法和静态属性就是我们无需实例化就可以调用 而对象的私有方法和属性,外部是不可以访问的 第二问:getName(); 直接调用getName函数。既然是直接调用那么就是访问当前上文作用域内的叫getName的函数,所以这里应该直接把关注点放在4和5上,跟1 2 3都没什么关系。

那这里是输出4还是5呢?-----------互动

这里就有两个坑了

一是变量声明提升

JavaScript 解释器中存在一种变量声明被提升的机制,也就是说函数声明会被提升到作用域的最前面,即使写代码的时候是写在最后面,也还是会被提升至最前面。 二是函数表达式和函数声明的区别

函数声明在JS解析时进行函数提升,因此在同一个作用域内,不管函数声明在哪里定义,该函数都可以进行调用。而函数表达式的值是在JS运行时确定,并且在表达式赋值完成后,该函数才能调用。

// 直观的例子
getName();// 函数声明
var getName = function(){
  console.log('函数表达式')
}
getName();// 函数表达式
function getName(){
  console.log('函数声明');
}
getName(); // 函数表达式

所以第二问的答案就是4,5的函数声明被4的函数表达式覆盖了

第三问:Foo().getName() 先执行了Foo函数,然后调用Foo函数的返回值对象的getName属性函数

Foo函数的第一句getName = function () { console.log(1); };是一句函数赋值语句,注意它没有var声明,

所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的console.log(4)函数,将此变量的值赋值为 function(){console.log(1)} 。

此处实际上是将外层作用域内的getName函数修改了。

注意:此处若依然没有找到会一直向上查找到window对象,若window对象中也没有getName属性,就在window对象中创建一个getName变量。

之后Foo函数的返回值是this,this的指向是由所在函数的调用方式决定的。而此处的直接调用方式,this指向window对象。

所以Foo函数返回的是window对象,相当于执行window.getName(),而window中的getName已经被修改为alert(1),所以最终会输出1

此处考察了两个知识点,一个是变量作用域问题,一个是this指向问题

关于this,this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象

所以第三问中实际上就是window在调用**Foo()**函数,所以this的指向是window

第四问:getName() 直接调用getName函数,相当于window.getName(),因为这个变量已经被Foo函数执行时修改了,遂结果与第三问相同,为1,也就是说Foo执行后把全局的getName函数给重写了一次,所以结果就是Foo()执行重写的那个getName函数

第五问:new Foo.getName() 此处考察的是JS的运算符优先级问题

MDN运算符优先级:developer.mozilla.org/zh-CN/docs/…

这题首先看优先级的第18和第17都出现关于new的优先级,new (带参数列表)比new (无参数列表)高比函数调用高,跟成员访问同级

new Foo.getName();的优先级是这样的
相当于是:
new (Foo.getName)();

// 点的优先级(18)比new无参数列表(17)优先级高
// 当点运算完后又因为有个括号(),此时就是变成new有参数列表(18),所以直接执行new,当然也可能有朋友会有疑问为什么遇到()不函数调用再new呢,那是因为函数调用(17)比new有参数列表(18)优先级低

.成员访问(18)->new有参数列表(18)
所以这里实际上将getName函数作为了构造函数来执行,遂弹出2。

第六问:(new Foo()).getName() 这一题比上一题的唯一区别就是在Foo那里多出了一个括号,这个有括号跟没括号我们在第五问的时候也看出来优先级是有区别的

1.与第四问的区别就是有括号无括号,这里带括号是new 有参数列表,new 有参数列表的优先级是18,点的优先级也是18,优先级相同按从左到右的顺序执行。
	2.所以这里是先执行有参数列表,再执行点的优先级,最后再函数调用
	3.这里涉及到一个知识点,fn作为构造函数有返回值,在JS中构造函数可以有返回值也可以没有
		a.没有返回值,返回实例化的对象
		b.有返回值,检查其返回值是否为引用类型。
			非引用类型,如基本类型(String,Number,Boolean,Null,Undefined)则与无返回值相同,实际返回其实例化对象。
			引用类型,实际返回值为这个引用类型
	4.这里fn函数返回的是this,this在构造函数中本来就代表当前实例化对象,最终fn函数返回实例化对象。最终调用,实例化对象的getValue函数,因为在Foo构造函数中没有为实例化对象添加任何属性,当前对象的原型对象(prototype)中寻找getName函数。所以输出3。

第七问:new new Foo().getName(); 还有优先级的问题,到这里其实答案已经不重要了,关键是否真的知道面试官在考察我们什么。

最终就是
new ((new Foo()).getName)();
new有参数列表(18)->new有参数列表(18)
先初始化Foo的实例化对象,然后将其原型上的getName函数作为构造函数再次new,所以最终结果为3
function yideng(){}
const a = {}, b = Object.prototype;
console.log(a.prototype === b);
console.log(Object.getPrototypeOf(a) === b);
console.log(yideng.prototype === Object.getPrototypeOf(yideng));
// 写出执行结果,并解释为什么?
// 答案
false true false

//解析:

1)a.prototype === b  =>false
prototype属性是只有函数才特有的属性,当你创建一个函数时,js会自动为这个函数加上prototype属性,值是一个空对象。而实例对象是没有prototype属性的。所以a.prototype是undefined,第一个结果为false。

2)Object.getPrototypeOf(a) === b =>true
首先要明确对象和构造函数的关系,对象在创建的时候,其__proto__会指向其构造函数的prototype属性
Object实际上是一个构造函数(typeof Object的结果为"function"),使用字面量创建对象和new Object创建对象是一样的,所以a.__proto__也就是Object.prototype,所以Object.getPrototypeOf(a)与a.__proto__是一样的,第二个结果为true

3)yideng.prototype === Object.getPrototypeOf(yideng) =>false
关键点:f.prototype和Object.getPrototypeOf(f)说的不是一回事

①f.prototype 是使用使用 new 创建的 f 实例的原型:
f.prototype === Object.getPrototypeOf(new f()); // true

②Object.getPrototypeOf(f)是 f 函数的原型:
Object.getPrototypeOf(f) === Function.prototype; //true

所以答案是 false

//知识点
__proto__(隐式原型)与prototype(显式原型)
1)是什么?
①显式原型 explicit prototype property:
每一个函数在创建之后都会拥有一个名为prototype的属性,这个属性指向函数的原型对象。(需要注意的是,通过Function.prototype.bind方法构造出来的函数是个例外,它没有prototype属性)
②隐式原型 implicit prototype link:
JavaScript中任意对象都有一个内置属性[[prototype]],在ES5之前没有标准的方法访问这个内置属性,但是大多数浏览器都支持通过__proto__来访问。ES5中有了对于这个内置属性标准的Get方法Object.getPrototypeOf()。(注意:Object.prototype 这个对象是个例外,它的__proto__值为null)
③二者的关系:
隐式原型指向创建这个对象的函数(constructor)的prototype

2)作用是什么?
①显式原型的作用:用来实现基于原型的继承与属性的共享。
②隐式原型的作用:构成原型链,同样用于实现基于原型的继承。举个例子,当我们访问obj这个对象中的x属性时,如果在obj中找不到,那么就会沿着__proto__依次查找。

3)__proto__的指向:
__proto__的指向到底如何判断呢?根据ECMA定义 'to the value of its constructor’s "prototype" ' ----指向创建这个对象的函数的显式原型。
所以关键的点在于找到创建这个对象的构造函数,接下来就来看一下JS中对象被创建的方式,一眼看过去似乎有三种方式:(1)对象字面量的方式 (2)new 的方式 (3)ES5中的Object.create()。
但是本质上只有一种方式,也就是通过new来创建。为什么这么说呢,首先字面量的方式是一种为了开发人员更方便创建对象的一个语法糖,本质就是 var o = new Object(); o.xx = xx;o.yy=yy;

加载页面和渲染过程 回答问题的时候,关键要抓住核心的要点,把要点说全面,然后再稍微加一些解析,要简明扼要,思路清晰,不能拖沓。

面试题目:浏览器从加载到渲染页面的过程 首先回答加载的流程,回答要点 浏览器根据 DNS 服务器得到域名的 IP 地址 向这个 IP 的机器发送 HTTP 请求 服务器收到、处理并返回 HTTP 请求 浏览器得到返回内容 稍加分析 例如在浏览器输入https//yidengxuetang.com的时候,首先经过 DNS 解析,yidengxuetang.com对应的 IP 是101.200.185.250,然后浏览器向该 IP 发送 HTTP 请求。 server 端接收到 HTTP 请求,然后经过计算(向不同的用户推送不同的内容),返回 HTTP 请求,返回的内容其实就是一堆 HMTL 格式的字符串,因为只有 HTML 格式浏览器才能正确解析,这是 W3C 标准的要求。 接下来就是渲染过程了,回答要点

<script>

加以分析 浏览器已经拿到了 server 端返回的 HTML 内容,开始解析并渲染。最初拿到的内容就是一堆字符串,必须先结构化成计算机擅长处理的基本数据结构,因此要把 HTML 字符串转化成 DOM 树 —— 树是最基本的数据结构之一。 解析过程中,如果遇到 和