掘金出的小册《前端面试之道》是本好书,但可能由于本人基础太差的原因一开始看不太懂,最近在找实习不得不硬啃,所以基于小册找了点资料查漏补缺。
内置类型
JS 中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object)。
基本类型有六种: null,undefined,boolean,number,string,symbol(ES6新增)
关于Symbol:表示独一无二的值,从根本上防止属性名的冲突。是一种类似于字符串的数据类型.
栈:原始数据类型(Undefined,Null,Boolean,Number、String) 堆:引用数据类型(对象、数组和函数(特殊对象)) 引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
let s = Symbol();
typeof s // "symbol"
复制代码
NaN 也属于 number 类型,并且 NaN 不等于自身。
创建对象
- 对象字面量
- 用function来模拟无参的构造函数
- 用function来模拟参构造函数来实现(用this关键字定义构造的上下文属性)
- 用工厂方式来创建(内置对象)
- 用原型方式来创建
Typeof
typeof 对于基本类型,除了 null 都可以显示正确的类型
typeof 对于对象,除了函数都会显示 object
对于 null 来说,虽然它是基本类型,但是会显示 object,这是一个存在很久了的 Bug
如果我们想获得一个变量的正确类型,可以通过
Object.prototype.toString.call(xx)。
类型转换
- 转Boolean:
在条件判断时,除了 undefined, null, false, NaN, '', 0, -0 ,其他所有值都转为 true,包括所有对象。
- 对象转基本类型:
首先会调用valueOf
然后调用 toString
。 (或者重写 Symbol.toPrimitive ,该方法在转基本类型时调用优先级最高。)
字符串转数字:parseFloat('12.3b');
- 四则运算符
只有当加法运算时,其中一方是字符串类型,就会把另一个也转为字符串类型。其他运算只要其中一方是数字,那么另一方就转为数字。
加法运算会触发三种类型转换: 值转换为原始值,转换为数字,转换为字符串。
- == 操作符
null == undefined //true
解析[] == ![] // -> true
// [] 转成 true,然后取反变成 false
[] == false
// 根据第 8 条得出
// 若Type(y) is boolean, compare x == ToNumber(y)
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
// if Type(x) is object and Type(y) is string or number, compare ToPrimitive(x) == y
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
复制代码
- 比较运算符
- 如果是对象,就通过 toPrimitive 转换对象
- 如果是字符串,就通过 unicode 字符索引来比较
原型
每个函数都有 prototype 属性,除了 Function.prototype.bind()
,该属性指向原型。
每个对象都有__proto__
属性,指向了创建该对象的构造函数的原型。 其实这个属性指向了 [[prototype]]
,但是 [[prototype]]
是内部属性,我们并不能访问到,所以使用__proto__
来访问。
对象可以通过__proto__
来寻找不属于该对象的属性, 因为__proto__
将对象连接起来组成了原型链。
总结:
Object
是所有对象的爸爸,所有对象都可以通过__proto__
找到它Function
是所有函数的爸爸,所有函数都可以通过__proto__
找到它Function.prototype
和Object.prototype
是两个特殊的对象,他们由引擎来创建- 除了以上两个特殊对象,其他对象都是通过构造器
new
出来的 - 函数的
prototype
是一个对象,也就是原型 - 对象的
__proto__
指向原型,__proto__
将对象和原型连接起来组成了原型链
JS中的原型和原型链(面试中奖率120%)
温故js系列(15)-原型&原型链&原型继承
JS原型链与继承别再被问倒了
原型链例子:
function Father(){
this.property = true;
}
Father.prototype.getFatherValue = function(){
return this.property;
}
function Son(){
this.sonProperty = false;
}
//继承 Father
Son.prototype = new Father();
//Son.prototype被重写,导致Son.prototype.constructor也一同被重写
Son.prototype.getSonVaule = function(){
return this.sonProperty;
}
var instance = new Son();
alert(instance.getFatherValue());//true
//instance实例通过原型链找到了Father原型中的getFatherValue方法.
复制代码
原型链存在的问题:
- 当原型链中包含引用类型值的原型时,该引用类型值会被所有实例共享;
- 在创建子类型(例如创建Son的实例)时,不能向超类型(例如Father)的构造函数中传递参数.
建议的继承方式:
- 使用借用构造函数+原型链 = 组合继承混合方式。
在子类构造函数内部使用apply或者call来调用父类的函数即可在实现属性继承的同时,又能传递参数,又能让实例不互相影响
function Super(){
this.flag = true;
}
Super.prototype.getFlag = function(){
return this.flag; //继承方法
}
function Sub(){
this.subFlag = flase
Super.call(this) //继承属性
}
Sub.prototype = new Super;
Sub.prototype.constructor = Sub;
var obj = new Sub();
Super.prototype.getSubFlag = function(){
return this.flag;
}
复制代码
小问题:Sub.prototype = new Super; 会导致Sub.prototype的constructor指向Super; 然而constructor的定义是要指向原型属性对应的构造函数的,Sub.prototype是Sub构造函数的原型,所以应该添加一句纠正:Sub.prototype.constructor = Sub;
组合继承是 JavaScript 最常用的继承模式; 不过, 它也有自己的不足。 组合继承最大的问题就是无论什么情况下, 都会调用两次父类构造函数: 一次是在创建子类型原型的时候, 另一次是在子类型构造函数内部。
- 寄生组合式继承 就是为了降低调用父类构造函数的开销而出现的。
基本思路是: 不必为了指定子类型的原型而调用超类型的构造函数。
function extend(subClass,superClass){
var prototype = object(superClass.prototype);//创建对象
prototype.constructor = subClass;//增强对象
subClass.prototype = prototype;//指定对象
}
复制代码
extend的高效率体现在它没有调用superClass构造函数,因此避免了在subClass.prototype上面创建不必要,多余的属性. 于此同时,原型链还能保持不变; 因此还能正常使用 instanceof 和 isPrototypeOf() 方法.
- ES6的class 其内部其实也是ES5组合继承的方式,通过call借用构造函数,在A类构造函数中调用相关属性,再用原型链的连接实现方法的继承
class B extends A {
constructor() {
return A.call(this); //继承属性
}
}
A.prototype = new B; //继承方法
复制代码
ES6封装了class,extends关键字来实现继承,内部的实现原理其实依然是基于上面所讲的原型链,不过进过一层封装后,Javascript的继承得以更加简洁优雅地实现
class ColorPoint extends Point {
//通过constructor来定义构造函数,用super调用父类的属性方法
constructor(x, y, color) {
super(x, y); // 等同于parent.constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 等同于parent.toString()
}
}
复制代码
new
调用new的过程会发生:
- 新生成了一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。
- 加入属性和方法。
- 返回新对象。
//自定义new
function create() {
// 创建一个空的对象
let obj = new Object()
// 获得构造函数
let Con = [].shift.call(arguments)
// 链接到原型
obj.__proto__ = Con.prototype
// 绑定 this,执行构造函数
let result = Con.apply(obj, arguments)
// 确保 new 出来的是个对象
return typeof result === 'object' ? result : obj
}
复制代码
对于创建一个对象来说,更推荐使用字面量的方式创建对象
(无论性能上还是可读性)。
因为使用 new Object() 的方式创建对象需要通过作用域链一层层找到 Object,但是你使用字面量的方式就没这个问题。
function Foo() {}
// function 就是个语法糖
// 内部等同于 new Function()
let a = { b: 1 }
// 这个字面量内部也是使用了 new Object()
复制代码
new Foo() 的优先级大于 new Foo
new Foo.getName(); // -> new (Foo.getName());
new Foo().getName();
// -> (new Foo()).getName();
//先执行 new Foo() 产生了一个实例,然后通过原型链找到了 Foo 上的 getName 函数
复制代码
instanceof
instanceof 可以正确的判断对象的类型,因为内部机制是通过判断对象的原型链中是不是能找到类型的 prototype。
this
补充:普通函数和构造函数的区别
- 构造函数也是一个普通函数,创建方式一样,但构造函数习惯上首字母大写
- 构造函数和普通函数的区别在于:调用方式不一样 普通函数的调用方式:直接调用 person(); 构造函数的调用方式:需要使用new关键字来调用 new Person();
- 构造函数的执行流程 A 立刻在堆内存中创建一个新的对象 B 将新建的对象设置为函数中的this C 逐个执行函数中的代码 D 将新建的对象作为返回值
- 普通函数例子:因为没有返回值,所以为undefined 构造函数例子:构造函数会马上创建一个新对象,并将该新对象作为返回值返回
JavaScript 中函数的调用有以下几种方式:作为对象方法调用,作为函数调用,作为构造函数调用,和使用 apply 或 call 调用。
参考: call、apply和bind方法的用法以及区别
- 作为对象调用
在 JavaScript 中,函数也是对象,因此函数可以作为一个对象的属性,此时该函数被称为该对象的方法,也就是当函数作为方法时,this 被绑定到该对象。
var point = {
x : 0,
y : 0,
moveTo : function(x, y) {
this.x = this.x + x;
this.y = this.y + y;
}
};
point.moveTo(1, 1)//this 绑定到当前对象,即 point 对象
复制代码
- 作为函数调用
函数直接被调用(比如回调函数),此时 this 绑定到全局对象。在浏览器中,window 就是该全局对象。
function makeNoSense(x) {
this.x = x;
}
makeNoSense(5);
//执行赋值语句,相当于隐式的声明了一个全局变量(不好)
x;// x 已经成为一个值为 5 的全局变量
复制代码
对于内部函数,即声明在另外一个函数体内的函数,这种绑定到全局对象的方式就会产生问题。
内部函数的 this 应该绑定到其外层函数对应的对象上,可以使用变量替代的方法,该变量一般被命名为 that。
var point = {
x : 0,
y : 0,
moveTo : function(x, y) {
var that = this;
// 内部函数
var moveX = function(x) {
that.x = x;
};
var moveY = function(y) {
that.y = y;
}
moveX(x);
moveY(y);
}
};
point.moveTo(1, 1);
point.x; //==>1
point.y; //==>1
复制代码
- 作为构造函数调用
JavaScript 支持面向对象式编程,与主流的面向对象式编程语言不同,JavaScript 并没有类(class)的概念,而是使用基于原型(prototype)的继承方式。
new 一个函数时,背地里会将创建一个连接到 prototype 成员的新对象,同时this会被绑定到那个新对象上。
- 使用apply或call调用
在 JavaScript 中函数也是对象,对象则有方法,apply 和 call 就是函数对象的方法。
apply 和 call 允许切换函数执行的上下文环境(context),即 this 绑定的对象。
bind() 函数会创建一个新函数(称为绑定函数)
- bind是ES5新增的一个方法,传参和call或apply类似
- 不会执行对应的函数,call或apply会自动执行对应的函数
- 返回对函数的引用
function Point(x, y){
this.x = x;
this.y = y;
this.moveTo = function(x, y){
this.x = x;
this.y = y;
}
}
//使用构造函数生成了一个对象 p1,该对象同时具有 moveTo 方法;
var p1 = new Point(0, 0);
//使用对象字面量创建了另一个对象 p2
var p2 = {x: 0, y: 0};
//使用 apply 可以将 p1 的方法应用到 p2 上,这时候 this 也被绑定到对象 p2 上
p1.moveTo(1, 1);
p1.moveTo.apply(p2, [10, 10]);
//另一个方法 call 也具备同样功能,不同的是最后的参数不是作为一个数组统一传入,而是分开传入的。
复制代码
总结: this总是指向函数的直接调用者(而非间接调用者); 作为函数调用时,this 绑定到全局对象;作为方法调用时,this 绑定到该方法所属的对象。 如果有new关键字,this指向new出来的那个对象; 在事件中,this指向触发这个事件的对象,特殊的是,IE中的attachEvent中的this总是指向全局对象Window;
几个规则:
//作为函数调用
function foo() {
console.log(this.a)
}
var a = 1
foo()
//作为方法调用
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上两者情况 `this` 只依赖于调用函数前的对象,优先级是第二个情况大于第一个情况
//作为构造函数调用
//以下情况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
复制代码
箭头函数中的this:
function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
//调用 a 符合前面代码中的第一个情况,所以 this 是 window。并且 this 一旦绑定了上下文,就不会被任何代码改变。
复制代码
箭头函数其实是没有 this 的,这个函数中的 this 只取决于他外面的第一个不是箭头函数的函数的 this。
执行上下文
- 作用域
在ES6之前,js没有块级作用域,只有全局和函数作用域。
注: 其实从 ES3 发布以来,JavaScript 就有了块级作用域(with 和 catch分句),而 ES6 引入了 let
变量提升:
console.log(a); //undefined
var a = 2;
//等同于
var a;
console.log(a);
a = 2;
复制代码
var a;
是定义声明,在编译阶段声明。a = 2;
是赋值声明,会被留在原地等待执行阶段。
函数提升:
在函数作用域中,局部变量的优先级比同名的全局变量高。 函数声明会被提升,函数表达式不会。
var a;
a = true;
function foo() {
var a;
if(a) {
a = 10;
}
console.log(a);
}
foo(); //undefined
复制代码
在 foo(...) {} 的函数作用域中,这个重名局部变量 a 会屏蔽全局变量 a,换句话说,在遇到对 a 的赋值声明之前,在 foo(...) {},a 的值都是 undefined! 所以一个 undefined 的 a 进入不了 if(a) {...} 中,最后被打印出来的是 undefined。
- 执行上下文
-
全局执行上下文
-
函数执行上下文
-
eval 执行上下文(并不经常使用)
执行栈:也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。
每个执行上下文中都有三个重要的属性
-
变量对象(VO),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问
-
作用域链(JS 采用词法作用域,也就是说变量的作用域是在定义时就决定了)
-
this
var 会产生很多错误,所以在 ES6中引入了 let。
let 不能在声明前使用.
并不是常说的 let 不会提升,let 提升了声明但没有赋值,因为临时死区导致了并不能在声明前使用。
在提升的过程中,相同的函数会覆盖上一个函数,并且函数优先于变量提升。
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
复制代码
- 作用域和执行上下文的关系
几乎是没有啥交集。
在一个函数被执行时,创建的执行上下文对象除了保存了些代码执行的信息,还会把当前的作用域保存在执行上下文中。所以它们的关系只是存储关系。
this 的值是通过当前执行上下文中保存的作用域(对象)来获取到的。