?这只是个人笔记……我没想到居然有人看到……
题目来源: 前端开发面试题
答案基本是自己整理的。并不是全部题目都有。
会有很多自己的啰嗦,还有很多乱七八糟的补充,请见谅。
介绍js的基本数据类型(原始数据类型)
Undefined、Null
Boolean、Number、String
//ECMAScript 2015新增:
Symbol //创建后独一无二且不可变的数据类型
复制代码
Symbol
参考:
以下多引自:MDN
Symbol 实例是唯一且不可改变的。
Symbol 对象是 Symbol原始值的封装。
Symbol 的描述是可选的,但仅用于调试目的。
Symbol("foo") !== Symbol("foo")
const foo = Symbol()
const bar = Symbol()
typeof foo === "symbol"
typeof bar === "symbol"
复制代码
- 为什么要引入symbol?
ES5 的对象属性名都是字符串,这容易造成属性名的冲突。
比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。
如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。
- 生成方式:
Symbol 值通过Symbol函数生成。
let s = Symbol()
typeof s // "symbol"
复制代码
Symbol函数前不能使用new命令,否则会报错。这是因为生成的Symbol是一个原始类型的值,不是对象。
- 参数
description
: 可选。string。对Symbol实例的描述,仅用于调试。
- 遍历
ES6之前的方法,无法获取。
const foo = Symbol()
const bar = Symbol()
let obj = {}
obj[foo] = "foo"
obj[bar] = "bar"
JSON.stringify(obj) // {}
Object.keys(obj) // []
Object.getOwnPropertyNames(obj) // []
Object.getOwnPropertySymbols(obj) // [ Symbol(), Symbol() ]
Reflect.ownKeys(obj) // [Symbol(), Symbol()]
复制代码
js有哪些内置对象?
划分一
基本对象:Object、Function、Boolean、Symbol、Error
数字和日期对象:Number、Math、Date
字符串:String、RegExp
集合对象:Array、Map、Set
结构化数据:JSON
控制抽象对象:Promise
其他:arguments
划分二
Object 是 JavaScript 中所有对象的父对象。
数据封装类对象:Object、Array、Boolean、Number 和 String
单例内置对象:Math、Global
其他对象:Function、Arguments、Date、RegExp、Error
说几条写JavaScript的基本规范?
-
不要在同一行声明多个变量。
-
使用
===
/!==
来比较true
/false
或者数值- 对于原始数据类型,==和===是有区别的
- 不同类型间比较,==只比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等
- 同类型比较,直接进行“值”比较,两者结果一样
- 对于Object,==和===没有区别, 进行“指针地址”比较
- 基础类型与Object,==和===有区别
==
将Object转化为基础类型,进行“值”比较- 因为类型不同,===结果为false
-
补充点:
==
和类型转换参考:JavaScript "loose" comparison step by step ←文章还提供了一个很好用的过程展示工具。
判断流程简单总结:流程图
注:关于其中涉及的
toPrimitive
和toNumber
函数,参考:从[]==![]
为true
来剖析JavaScript各种蛋疼的类型转换 和 MDN
- 对于原始数据类型,==和===是有区别的
-
使用对象字面量替代new Array这种形式
-
不要使用全局函数
-
Switch语句必须带有default分支
-
函数不应该有时候有返回值,有时候没有返回值
-
For循环必须使用大括号
-
If语句必须使用大括号
-
for-in循环中的变量应该使用var关键字明确限定作用域,从而避免作用域污染。
JavaScript原型,原型链 ? 有什么特点?
原型
All ordinary objects have an internal slot called [[Prototype]].
The value of this internal slot is either null or an object and ++is used for implementing inheritance++.
Data properties of the [[Prototype]] object are inherited (are visible as properties of the child object) for the purposes of get access, but not for set access. Accessor properties are inherited for both get access and set access.
每个对象有一个私有属性,记作[[prototype]]
。
本质:一个链接到其他对象的引用。
作用:用于继承。
被链接的对象,是该对象的原型对象。
原型链
而原型对象又会有自己的原型对象,这就形成了原型链。
原型链的顶端:Object.prototype
.(内置了很多功能和方法,包括.toString()
, .valueOf()
, .hasOwnProperty()
, .isPrototypeOf()
等等。)
当我们访问一个对象的属性a
时,如果这个对象内部不存在a
这个属性,那么就会去原型对象里找a
这个属性。如果原型对象里还找不到,则会沿着原型链一路向上,直到找到这个a
为止;若到达原型链顶端,仍未找到,则停止并返回undefined。
疑问:Object.prototype
VS null, 哪个才是原型链的末端?
MDN上提到:
By definition, null has no prototype, and acts as the final link in this prototype chain.
但 You Don't Know JS 又说,Object.prototype
是原型链的顶端。
这两种其实本质是一样的,只是看的方式不同。
就像俄罗斯套娃,最后一个的内部是空的,所以可以断定它是“最后一个”。如果用这种眼光去看,那么Object.prototype
就是顶端。
而如果把null也当成是一个节点,那么“空”才是原型链的顶端。
但也有人提出:
ECMA262这么写道:
The value of the [[Prototype]] internal slot of the Object prototype object is null.
我不是很懂,因为这个初始值是null,所以就自然成为原型链的顶端了吗
function的prototype属性
-
关系:
instance.constructor.prototype === instance.__proto__
这个关系不一定正确!!!
比如:
let a = Object.create(null) a.constructor //undefined let b = Object.create(a) b.constructor //undefined 复制代码
用
Object.create
的对象,原型链上不一定有Object.prototype
,所以也没有默认的.constructor
、.__proto__
等等。 -
是否所有函数都有
.prototype
属性Function instances that ++can be used as a constructor++ have a prototype property. Whenever such a function instance is created another ordinary object is also created and is the initial value of the function’s prototype property. Unless otherwise specified, the value of the prototype property is used to initialize the [[Prototype]] internal slot of the object created when that function is invoked as a constructor.
This property has the attributes { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }.
NOTE:
//下面写了没有这个属性的三种情况
Function objects created using
Function.prototype.bind
, or by evaluating a MethodDefinition (that are not a GeneratorMethod) or an ArrowFunction grammar production do not have a prototype property.可以当作构造函数的function都有一个属性prototype,这个属性默认指向一个JS私有对象
%FunctionPrototype%
。 -
.prototype
属性和[[prototype]]
的关系 -
也是
-
改变
[[prototype]]
的两种方法When a constructor creates an object, that object implicitly references the constructor’s prototype property for the purpose of resolving property references. The constructor’s prototype property can be referenced by the program expression constructor.prototype, and properties added to an object’s prototype are shared, through inheritance, by all objects sharing the prototype.
Alternatively, a new object may be created with an explicitly specified prototype by using the
Object.create
built-in function.1、使用构造函数创建
obj
,会隐式地引用构造函数的prototype
属性,作为obj
的[[prototype]]
。(可以这么理解:
obj.[[prototype]] = objConstructor.prototype
)2、
Object.create(O)
可创建一个obj,并指定它的[[prototype]]
为O
。- 另外两种改变
[[prototype]]
的方法:详见MDN- ES6: Object.setPrototypeOf
- proto(不推荐使用这个属性)
- 另外两种改变
特点:
JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本。当我们修改原型时,与之相关的对象也会继承这一改变。
当我们需要一个属性的时,Javascript引擎会先看当前对象中是否有这个属性, 如果没有的话, 就会查找他的Prototype对象是否有这个属性,如此递推下去,一直检索到 Object 内建对象。
function Func() {}
Func.prototype.name = "Sean";
Func.prototype.getInfo = function() { return this.name; }
// pre ES5.1
var person = new Func();
//ES5.1
// var person = Object.create(Func.prototype);
//ES6+
// Object.setPrototypeOf(person, Func.prototype);
//它拥有了Func的属性和方法
console.log(person.getInfo()); //"Sean"
console.log(Func.prototype); // Func { name="Sean", getInfo=function()}
复制代码
JavaScript有几种类型的值?你能画一下他们的内存图吗?
- 原始数据类型(Undefined,Null,Boolean,Number、String)
- 引用数据类型(对象、数组和函数)
两种类型的区别:存储位置不同。
区别 | 原始数据类型 | 引用数据类型 |
---|---|---|
存储位置 | 栈(stack) | 堆(heap) |
占据空间 | 小,大小固定 | 大,大小不固定 |
引用数据类型如果存储在栈中,将会影响程序运行的性能;
引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。
如何将字符串转化为数字,例如'12.3b'?
1. 转换函数
// radix = 0, 2, 3, ..., 36
// radix表示的是string的进制,而非转换出来的结果的进制
parseInt(string, radix)
parseFloat(value)
复制代码
都为全局函数,不属于任何对象。(但Number里有同名函数)
能转换的数据类型:
- String
- 定义了 toString 或者 valueOf 方法的对象
Examples:
parseInt('36', 36) // 114
parseInt('36', 37) // NaN
parseInt('36', 1) // NaN
parseInt('36', 0) // 36。radix为0或null,则默认为十进制
let obj = {}
obj.toString = function () { return '3'; }
parseInt(obj) // 3
复制代码
2. 强制类型转换(type casting)
Number(value)
复制代码
在非构造器上下文中 (如:没有 new 操作符),Number 能被用来执行类型转换。
3. 正则表达式 + 隐式转换
'12.3b'.match(/(\d)+(\.)?(\d)+/g)[0] * 1
复制代码
但是这个不太靠谱,提供一种思路而已。
如何将浮点数点左边的数每三位添加一个逗号,如12000000.11转化为『12,000,000.11』?
方法一、toLocaleString
参考:
var number = 12345.543;
number.toLocaleString('en'); // "12,345.543"
number = 123.1;
number.toLocaleString('en'); // "123.1"
number = 12345.54321;
number.toLocaleString('en'); // "12,345.543"
复制代码
局限:只显示三位小数,可通过maximumSignificantDigits
这个参数调整,但老的浏览器不支持此参数。
方法二、正则表达式
参考:javascript 正则(将数字转化为三位分隔的样式)
不易懂,但兼容性强。
写法一:
// 匹配边界,直接可替换成逗号
function commafy (num) {
return num
.toString()
.replace(/\B(?=(?:\d{3})+\.)/g, ',')
}
复制代码
写法二:
// 匹配边界前的那个数字,所以不能直接替换,
// 要保留数字,在数字后加一个逗号。
function commafy(num){
return num && num
.toString()
.replace(/(\d)(?=(\d{3})+\.)/g, function($1, $2){
return $2 + ',';
});
}
// 也可以这么写(不用function用string)
replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
复制代码
如何实现数组的随机排序?
方法一:Knuth-Fisher-Yates shuffle algorithm
The de-facto(事实上) unbiased(无偏的) shuffle algorithm.(参考)
function shuffle(array) {
var currentIndex = array.length,
temporaryValue,
randomIndex;
// While there remain elements to shuffle...
while (0 !== currentIndex) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
// And swap it with the current element.
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// Used like so
var arr = [2, 11, 37, 42];
arr = shuffle(arr);
console.log(arr);
复制代码
!!!注意:The Danger of Naïveté
注意以下两者的区别!
// naïve algorithm
// i = 0, 1, ..., len-1 (循环len次)
// 每次rand.Next()的值不变,都为cards.Length
for (int i = 0; i < cards.Length; i++) {
int n = rand.Next(cards.Length);
Swap(ref cards[i], ref cards[n]);
}
// correct Knuth-Fisher-Yates shuffle algorithm
// i = 1, 2, ..., len-1 (循环len-1次)
// 每次rand.Next()的值都不同,为 i + 1 (即2, 3, ..., len)
for (int i = cards.Length - 1; i > 0; i--) {
int n = rand.Next(i + 1);
Swap(ref cards[i], ref cards[n]);
}
复制代码
The naive shuffle results in 33 (27) possible deck combinations. That's odd, because the mathematics tell us that there are really only 3! or 6 possible combinations of a 3 card deck.
In the KFY shuffle, we start with an initial order, swap from the third position with any of the three cards, then swap again from the second position with the remaining two cards.
naive shuffle: 3次rand.Next(3)
,也就是说,每次都是 0, 1, 2
三个数中,随机选一个。也就是总的结果有==3^3=27==种。
KFY shuffle:只有是rand.Next(3)
和是rand.Next(2)
,第一次是 0, 1, 2
三个数中,随机选一个,第二次是 0, 1
两个数中选一个。结果为==3!=6==种。
本质:完美洗牌的算法问题。
这个问题需要剔除给定的array的初始顺序的影响。
无论怎么洗,出现各种结果的概率必须是一样的。
这样其实问题就简单:只要考虑,每个index的位置,有多少种可能性就行了。
这就像是:餐桌四个座位,有几种坐法?
其实就是array的排列问题。
方法二:push + 删减
var arr = [1,2,3,4,5,6,7,8,9,10];
function randSort2(arr){
var mixedArray = [];
while(arr.length > 0){
var randomIndex = parseInt(Math.random()*arr.length);
mixedArray.push(arr[randomIndex]);
// 删除已push的项目
arr.splice(randomIndex, 1);
}
return mixedArray;
}
console.log(randSort2(arr));
复制代码
方法三:sort
Array.prototype.sort([compareFunction])
: 根据所提供的排序算法,进行排序。
compareFunction(a, b)
接受两个参数,即两个比较的item。
函数返回一个number:
- 结果为0则保持不变
- 小于0,a在前,b在后
- 大于0,a在后,b在前
var arr = [1,2,3,4,5,6,7,8,9,10];
arr.sort(function(){
return Math.random() - 0.5;
})
console.log(arr);
复制代码
-
缺陷:
这个方法产生的结果,并不是均匀的。
测试如下:
function randSort(){ var sum = Array.apply(null, { length: 10 }).fill(0); var AVG_TIMES = 1000; var SHUFFLE_TIMES = 10000; var avgTimes = AVG_TIMES; while (avgTimes) { var times = SHUFFLE_TIMES; var counts = Array.apply(null, { length: 10 }).fill(0); // 计算index0里各个数字的出现概率 while (times) { var someArray = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]; someArray.sort(() => Math.random() - 0.5); counts[someArray[0]]++; times--; } //console.log(counts); sum = sum.map(function (v, i) { return v + counts[i]; }); avgTimes--; } console.log(sum.map(function (v, i) { return v / AVG_TIMES; })); } 复制代码
测试了shuffle后的array[0]的取值中,各个数字在10000次测试中的平均出现次数。
1000 * 10000 次的平均结果是这样:
[1953.077, 725.763, 963.728, 1287.405, 829.722, 893.497, 990.991, 1115.342, 599.359, 641.116] 复制代码
javascript创建对象的几种方式?
参考:高程三 6.2
0、Object 构造函数或对象字面量
可以用来创建单个对象。
但要创建很多对象,就会产生大量的重复代码。
// 创建 Object 的实例
var person = new Object();
person.firstname = 'Captain';
person.lastname = 'Flag';
person.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
// 字面量
var person = {
firstname: 'Captain',
lastname: 'Flag',
sayName: function sayName () {
console.log(this.firstname + ' ' + this.lastname);
}
};
复制代码
1、工厂模式
工厂模式是软件工程领域一种广为人知的设计模式,这种模式抽象了创建具体对象的过程(本书后 面还将讨论其他设计模式及其在 JavaScript 中的实现)。考虑到在 ECMAScript 中无法创建类,开发人员 就发明了一种函数,用函数来封装以特定接口创建对象的细节
function createPerson (firstname, lastname) {
var o = new Object ();
o.firstname = firstname;
o.lastname = lastname;
o.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
return o;
}
var cap = createPerson('Captain', 'Flag');
复制代码
- 优点:解决了创建多个相似对象的问题。
- 缺点:没有解决对象识别的问题(即怎样知道一个对象的类型)。
2、构造函数模式
function Person (firstname, lastname) {
this.firstname = firstname;
this.lastname = lastname;
this.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
}
var cap = new Person('Captain', 'Flag');
复制代码
new 操作符调用构造函数的过程:
- 创建一个新对象;
- 将新对象的
[[protorype]]
设置为Person.prototype
; - 构造函数
Person
被传入参数并调用(执行构造函数中的代码,为这个新对象添加属性),关键字this
被设定指向新对象; - 除非函数
Person
显性返回一个对象,否则自动返回新对象。
-
优点:
有解决对象识别的问题,可以将实例标识为一种特定的类型。
-
缺点:
无法进行函数复用。(以这种方式创建函数,会导致不同的作用域链和标识符解析,但创建 Function 新实例的机制仍然是相同的。因此,不同实例上的同名函数是不相等的。但创建两个完成同样任务的 Function 实例的确没有必要。)
解决:把函数定义转移到构造函数外部。
function sayName () { console.log(this.firstname + ' ' + this.lastname); } function Person (firstname, lastname) { this.firstname = firstname; this.lastname = lastname; this.sayName = sayName; } 复制代码
新的问题:破坏封装。
3、原型模式
可以当作构造函数的function都有一个属性prototype,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function Person () {}
Person.prototype.firstname = 'Captain';
Person.prototype.lastname = 'Flag';
Person.prototype.sayName = function sayName () {
console.log('Captain Flag');
};
var cap = new Person();
// 更简单的原型语法
function Person () {}
Person.prototype = {
// 这种写法会将默认的Person.prototype完全覆盖重写,
// Person也不会有`constructor`属性
// `Person.prototype.hasOwnProperty('constructor')`的结果为false
// 如果仍需要`constructor`属性,自行增加
constructor: Person,
firstname: 'Captain',
lastname: 'Flag',
sayName: function sayName () {
console.log('Captain Flag');
}
};
// 以上一种方式重设 constructor 属性会导致它的[[Enumerable]]特性被设置为 true
// 默认情况下,原生的 constructor 属性是不可枚举的
// 兼容 ECMAScript 5 的 JavaScript 引擎上,可用 Object.defineProperty() 修复
function Person () {}
Person.prototype = {
firstname: 'Captain',
lastname: 'Flag',
sayName: function sayName () {
console.log('Captain Flag');
}
};
// 重设constructor属性,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, 'constructor', {
enumerable: false,
value: Person
});
复制代码
注:后两者写法,都直接重写了原型,如果有实例在原型重写之前生成,这些实例与更改后的原型是没有联系的。
-
优点:
可以让所有对象实例共享原型对象所包含的属性和方法(不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中)。
-
缺点:
- 不能传递参数。
- 原型模式的最大问题是由其共享的本性所导致的。原型中所有属性是被很多实例共享的。
- 这种共享十分适用于函数,
- 对于原始数据类型尚可,
- 但引用类型在原型中也只是保留了一个指针,在实例中被修改后,会影响原型的值。
function Person () {} Person.prototype.arr = [1,2,3]; var cap = new Person(); cap.arr.push('a'); console.log(Person.prototype.arr); // [1, 2, 3, "a"] 复制代码
4、组合使用构造函数模式和原型模式
模式 | 所定义的内容 |
---|---|
构造函数模式 | 实例属性 |
原型模式 | 方法和共享的属性 |
定义引用类型的一种默认模式。
function Person (firstname, lastname) {
this.name = [firstname, lastname];
}
Person.prototype.sayName = function sayName () {
console.log(this.name.join(' '));
};
var cap = new Person('Captain', 'Flag');
cap.name.push('1911');
cap.sayName(); // Captain Flag 1911
复制代码
-
优点:
- 支持向构造函数传递参数。
- 每个实例都会有自己的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。
5、动态原型模式
function Person (firstname, lastname) {
this.name = [firstname, lastname];
// 只在 sayName() 方法不存在的情况下,才会将它添加到原型中。
// 这段代码只会在初次调用构造函数时才会执行。
// 此后,原型已经完成初始化,不需要再做什么修改了。
// 其中, if 语句检查的,可以是初始化之后应该存在的任何属性或方法,
// 并且不必检查每个属性和每个方法,只要检查其中一个即可。
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function sayName () {
console.log(this.name.join(' '));
};
}
}
var cap = new Person('Captain', 'Flag');
cap.name.push('1911');
cap.sayName(); // Captain Flag 1911
复制代码
-
优点:
有其他 OO 语言经验的开发人员在看到独立的构造函数和原型时,很可能会感到非常困惑。动态原 型模式正是致力于解决这个问题的一个方案,它把所有信息都封装在了构造函数中,而通过在构造函数 中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过 检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
- 对于采用这种模式创建的对象,还可以使用 instanceof 操作符确定它的类型
6、寄生构造函数模式
基本思想是创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象;但从表面上看,这个函数又很像是典型的构造函数。
function Person (firstname, lastname) {
var o = new Object ();
o.firstname = firstname;
o.lastname = lastname;
o.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
return o;
}
var cap = new Person('Captain', 'Flag');
复制代码
工厂模式 + 构造函数(除了使用 new 操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。)
构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return 语句,可以重写调用构造函数时返回的值。
-
优点:
在特殊的情况下用来为对象创建构造函数。
比如:创建一个具有额外方法的特殊数组。由于不能直接修改 Array 构造函数,因此可以使用这个模式。
-
缺点:
返回的对象与构造函数或者与构造函数的原型属性之间没有关系。不能依赖 instanceof 操作符来确定对象类型。
7、稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。稳妥对象最适合在一些安全的环境中(这些环境中会禁止使用 this 和 new),或者在防止数据被其他应用程序(如 Mashup 程序)改动时使用。
稳妥构造函数遵循与寄生构造函数类似的模式,但有两点不同:
- 一是新创建对象的实例方法不引用 this;
- 二是不使用 new 操作符调用构造函数。
function Person (firstname, lastname) {
// 创建要返回的对象
var o = new Object ();
// 定义私有变量和函数
o.firstname = firstname;
o.lastname = lastname;
// 添加方法
o.sayName = function sayName () {
console.log(this.firstname + ' ' + this.lastname);
};
// 返回对象
return o;
}
var cap = Person('Captain', 'Flag');
复制代码
-
优点:
安全性高,使得它非常适合在某些安全执行环境。
-
缺点:
与寄生构造函数模式类似,创建的对象与构造函数之间也没有什么关系,因此 instanceof 操作符对这种对象也没有意义。
Javascript如何实现继承?
参考:高程三 6.3
现在有一个"动物"对象的构造函数。
function Animal () {
this.kingdom = 'animal'; // 动物界
}
复制代码
还有一个"猫"对象的构造函数。
function Cat () {
this.family = 'cat'; // 猫科
}
复制代码
1、原型继承
ECMAScript 中描述了原型链的概念,并将原型链作为实现继承的主要方法。其基本思想是利用原型 让一个引用类型继承另一个引用类型的属性和方法。
如果"猫"的prototype对象,指向一个Animal的实例,那么所有"猫"的实例,就能继承Animal了。
// 删除了 prototype 对象原先的值
// 将Cat的prototype对象指向一个Animal的实例
Cat.prototype = new Animal();
// 任何一个prototype对象都有一个constructor属性,指向它的构造函数
// 如果没有上一行,Cat.prototype.constructor 指向Cat
// 加了这一行以后,Cat.prototype.constructor指向Animal
// 这显然会导致继承链的紊乱(cat明明构造函数Cat生成的),因此我们必须手动纠正
Cat.prototype.constructor = Cat;
var cat = new Cat();
console.log(cat.kingdom); // 'animal'
复制代码
缺点:
- 需要执行和建立Animal的实例,消耗内存,效率较低;
- 原型属性会被所有实例共享,在子类型中更改引用类型数据的值,会影响原型的值;
- 在创建子类型(Cat)的实例时,没有办法在不影响所有对象实例的情况下,给超类型(Animal)的构造函数传递参数。
2、借用构造函数(constructor stealing)/伪造对象/经典继承
在子类型构造函数的内部调用超类型构造函数。
函数只不过是在特定环境中执行代码的对象,因此通过使用 apply()
和 call()
方法也可以在(将来)新创建的对象上执行构造函数。
使用call或apply方法,将父对象的构造函数绑定在子对象上。
即在子对象构造函数中加一行:
function Cat () {
Animal.apply(this); // “借调”超类型的构造函数
// or
// Animal.call(this);
this.family = 'cat';
}
复制代码
-
优点:
-
简单。
-
可以在子类型构造函数中向超类型构造函数传递参数,如:
function Animal (name) { this.name = name; } function Cat (name) { this.kind = 'cat'; Animal.call(this, name); } 复制代码
-
-
缺点:
- 如果仅仅是借用构造函数,那么也将无法避免构造函数模式(创建对象的一种方式)存在的问题 —— 方法都在构造函数中定义,因此函数复用就无从谈起了。
- 在超类型的原型
Animal.prototype
中定义的方法(因为并没有利用原型链),对子类型而言也是不可见的,结果所有类型都只能使用构造函数模式。
3、组合继承(combination inheritance)/伪经典继承
JavaScript 中最常用的继承模式。
结合前两种方法,发挥二者之长的一种继承模式。
思路:
- 原型链:继承 原型属性和方法,
- 借用构造函数:继承 实例属性。(在子类型中更改其值,不会影响原型的值)
这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function Animal (name) {
this.name = name;
}
Animal.prototype.sayNamw = function () {
console.log(this.name);
};
function Cat (name) {
Animal.call(this, name); // 第二次调用超类构造函数
}
Cat.prototype = new Animal(); // 第一次调用超类构造函数
Cat.prototype.constructor = Cat;
// 使用
var cat1 = new Cat('Meow');
cat1.name; // 'Meow'
cat1.name = 'Oops'; // 'Oops'
Animal.prototype.name; // undefined(改变子类的值,不会改变原型的值)
cat1.sayName(); // 'Oops'
Animal.prototype.sayName(); // undefined
复制代码
instanceof
和 isPrototypeOf()
能够识别 基于组合继承创建的对象。
-
缺点:
-
无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是在子类型构造函数内部。
-
属性也会创建两遍:
- 第一次调用
Animal
构造函数时,Cat.prototype
会得到name属性; - 第二次调用
Animal
构造函数,在新对象上cat1
上创建了实例属性name
。 - 于是,这个属性就屏蔽了原型中的同名属性。
- 第一次调用
-
4、原型式继承
这种方法并没有使用严格意义上的构造函数。
借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
- ECMAScript 5 通过新增
Object.create()
方法规范化了原型式继承。
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
复制代码
object(o)
的作用是,以o为原型,创建一个新的对象。比如:
var oldObj = { a: 1 };
var newObj = object(oldObj);
复制代码
ECMAScript 5 通过新增Object.create()
方法规范化了原型式继承。
这个方法接收两个参数:一 个用作新对象原型的对象,(可选的)一个为新对象定义额外属性的对象。
-
缺点:
- 包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
var arr = ['old']; var oldObj = { arr: arr }; var newObj = object(oldObj); newObj.arr.push('new'); // 会更改原值 console.log(arr); // ['old', 'new'] newObj.arr = [0, 1]; // 不会更改原值 console.log(newObj.arr); // [0, 1] console.log(arr); // ['old', 'new'] 复制代码
- 包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。
5、寄生式(parasitic)继承
与原型式继承(上一种) 紧密相关的一种思路。
与寄生构造函数和工厂模式类似。
创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
// 原型式继承的主要方法
function object (o) {
function F () {}
F.prototype = o;
return new F();
}
function createAnother (original) {
// object()函数不是必需的;
// 任何能够返回新对象的函数都适用于此模式。
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){ //以某种方式来增强这个对象
console.log("hi");
};
return clone; //返回这个对象
}
复制代码
在主要考虑对象而不是自定义类型和构造函数的情况下,寄生式继承也是一种有用的模式。
-
缺点:
- 使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率;这一点与构造函数模式类似。
6、寄生组合式继承
解决3、组合继承
的缺点 —— 调用两次超类型构造函数 ∴ 属性也创建了两遍。
借用构造函数来继承属性,通过原型链的混成形式来继承方法。
-
基本思路:
不必为了 指定子类型的原型 而 调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。
-
本质:
使用 寄生式继承 来 继承超类型的原型,然后再将结果指定给子类型的原型。
/*
* @param {constructor} subType 子类型构造函数
* @param {constructor} superType 超类型构造函数
*/
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
复制代码
第一步是创建超类型原型的一个副本。
第二步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。
最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype()
函数的语句,去替换前面例子中为子类型原型赋值的语句了。
用inheritPrototype
替代3、组合继承
中,第一次调用构造函数时的Cat.prototype = new Animal();
:
function Animal (name) {
this.name = name;
}
Animal.prototype.sayNamw = function () {
console.log(this.name);
};
function Cat (name) {
Animal.call(this, name); // 这里保持不变,依旧会调用超类构造函数
}
//Cat.prototype = new Animal(); // 第一次调用超类构造函数
//Cat.prototype.constructor = Cat;
inheritPrototype(Cat, Animal);
复制代码
-
优点:
-
高效率,体现在它只调用了一次超类构造函数,因此避免了在
Cat.prototype
上创建不必要的、多余的属性,并且原型链还能保持不变还能够正常使用 instanceof 和 isPrototypeOf()。 -
开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。
-
Javascript作用链域?
全局函数无法查看局部函数的内部细节,但局部函数可以查看其上层的函数细节,直至全局细节。
当需要从局部函数查找某一属性或方法时,如果当前作用域没有找到,就会上溯到上层作用域查找,直至全局函数。
这种组织形式就是作用域链。
谈谈This对象的理解。
- this总是指向函数的直接调用者(而非间接调用者);
- 如果有new关键字,this指向new出来的那个对象;
- 在事件中,this指向触发这个事件的对象,特殊的是,IE中的attachEvent中的this总是指向全局对象Window。
以下内容总结自You Don't Know JS:
规则1. 默认绑定(Default Binding)
其他任何规则都无法应用时,就会使用默认绑定。
默认绑定情况下,this
在非严格模式中指向global
,严格模式中为undefined
。
function foo () {
'use strict';
console.log(this.a);
}
foo(); //Uncaught TypeError: Cannot read property 'a' of undefined
复制代码
规则2. 隐式绑定
考虑调用位置(call-site)是否有上下文对象(context object)。
this
总是指向函数的直接调用者(而非间接调用者)。
var obj = {
a: 1,
foo: function () {
console.log(this.a);
}
};
obj.foo(); // 1
复制代码
链式调用,调用的是最后一个对象的属性:
var obj1 = {
a: 1,
obj2: obj2
};
var obj2 = {
a: 2,
foo: function foo () {
console.log(this.a);
}
};
obj1.obj2.foo(); // 2
复制代码
隐式丢失(Implicitly Lost)
-
function reference/alias(函数引用)
var obj = { a: 1, foo: function () { console.log(this.a); } }; var bar = obj.foo; var a = 'oops, global'; bar(); // 'oops, global' 复制代码
其实,bar就是一个函数foo的引用而已。
它的调用位置其实是全局环境(默认绑定规则被应用)。
-
passing a callback function(传递回调函数)
function foo () { console.log(this.a); } function doSth (fn) { fn(); } var obj = { a: 1, foo: foo }; var a = 'oops, global'; doSth(obj.foo); // 这里其实就是对fn的赋值(LHS),本质同上一情形 复制代码
规则3. 显式绑定(Explicit Binding)
使用call()
或apply()
。
function foo () {
console.log(this.a);
}
var obj = {
a: 2
};
foo.call( obj );
复制代码
修正隐式绑定中的两个例子: 1. javascript var obj = { a: 1, foo: function () { console.log(this.a); } }; var bar = obj.foo; var a = 'oops, global'; bar(); // 'oops, global' bar.call(obj); // 1
2. ```javascript function foo () { console.log(this.a); } function doSth (fn) { //fn(); fn.call(obj); // 显式绑定 } var obj = { a: 1, foo: foo }; var a = 'oops, global';
doSth(obj.foo); // 这里其实就是对fn的赋值(LHS),本质同上一情形
```
复制代码
解决隐式绑定this丢失问题:Hard binding
上述的修正只是在调用的时候做的修正,没有真正解决问题。
-
包装成一个函数:
function foo () { console.log(this.a); } var obj = { a: 2 }; /* hard binding */ var bar = function () { foo.call( obj ); }; /* 如论怎么调用bar,都会调用obj.foo */ bar();// 2 setTimeout(bar, 100);// 2 bar.call(window);// 2 复制代码
传参:
function foo (b) { console.log(this.a, b); } var obj = { a: 1 }; var bar = function () { foo.apply(obj, arguments); }; bar(3); // 1 3 复制代码
-
reusable helper
function foo (b) { console.log(this.a, b); } var obj = { a: 1 }; // 可进行重复利用 function bind (fn, obj) { return function () { fn.apply(obj, arguments); }; } var bar = bind(foo, obj); bar(3); // 1 3 复制代码
ES5内置
Function.prototype.bind
:function foo (b) { console.log(this.a, b); } var obj = { a: 1 }; var bar = foo.bind(obj); bar(3); // 1 3 复制代码
-
一些API提供thisArg参数
function foo (el) { console.log(el, this.a); } var obj = { a: 'a' [1, 2, 3].forEach(foo, obj); // 1 "a" // 2 "a" // 3 "a" 复制代码
规则4. new绑定(new Binding)
将this指向新建的对象。
规则覆盖顺序
3显式、4new > 2隐式 > 1基础
例外
-
this被忽略
传递
null
或undefined
作为apply
、call
或bind
的绑定参数,this绑定会被忽略,而应用默认绑定。-
使用场景:
function foo () { // arguments为类数组方法,不能直接调用join方法 console.log(Array.prototype.join.call(arguments, ', ')) } // 数组转化为参数 foo.apply(null, [1,2]); // 1, 2 foo.apply(null, [1,2,3,4,5]); // 1, 2, 3, 4, 5 复制代码
-
可能存在的问题:
使用第三方库时,可能内部存在
this
,从而不经意中指向了global
。 -
更安全的this:
使用DMZ对象(一个完全空的、没有任何继承的对象)。
function foo () { console.log(this); console.log(Array.prototype.join.call(arguments, ', ')); } // DMZ empty object var Ø = Object.create(null); foo.apply(Ø, [1,2]) // {} No properties // 1, 2 foo.apply(null, [1,2]) // Window {postMessage: ƒ, blur: ƒ, focus: ƒ, close: ƒ, parent: Window, …} // 1, 2 复制代码
-
-
间接引用
退回默认绑定。
-
知识点:
变量声明返回undefined,赋值返回等号右边的值。
-
例子:
function foo () { console.log(this.a); } var a = 2; var o = { a: 3, foo: foo }; var p = { a: 4 }; (p.foo = o.foo)(); // 2 复制代码
why?
p.foo = o.foo
默认返回foo函数,这个foo函数才是立即执行的对象。p.foo = o.foo //ƒ foo () { // console.log(this.a); //} 复制代码
-
-
软绑定(Softening Binding)
复杂,暂时不想管。
词法this(Lexical this)
this是在定义函数时绑定的,不是在执行过程中绑定的。
-
方法一:词法捕获this
function foo () { var self = this; return function () { console.log(self.a); }; } var obj1 = { a: 1 }; var obj2 = { a: 2 }; var bar = foo.call(obj1); bar(); // 1 bar.call(obj2); // 1 复制代码
-
方法二:ES6 箭头函数
几乎与词法捕获相同。
function foo () { return () => { console.log(this.a); }; } var obj1 = { a: 1 }; var obj2 = { a: 2 }; var bar = foo.call(obj1); bar(); // 1 bar.call(obj2); // 1 复制代码
注意:尽量不要混用词法方法和默认的this机制(rule1-4)。
new操作符具体干了什么呢?
- 创建一个新对象;
- 将新对象的
[[protorype]]
设置为Base.prototype
; - 构造函数
Base
被传入参数并调用(执行构造函数中的代码,为这个新对象添加属性),关键字this
被设定指向新对象; - 除非函数
Base
显性返回一个对象,否则自动返回新对象。
new Base();
// 即
var obj = {};
obj.__proto__ = Base.prototype;
Base.call(obj);
复制代码
Ajax 是什么? 如何创建一个Ajax?
参考: MDN,高程三
ajax的全称:Asynchronous Javascript And XML(异步传输+js+xml)。
其本身不是一种新技术,而是一个在 2005年被Jesse James Garrett提出的新术语,用来描述一种使用现有技术集合的‘新’方法。包括:
- HTML or XHTML
- CSS(Cascading Style Sheets)
- JavaScript
- DOM(The Document Object Model)
- XML
- XSLT
- 最重要的 XMLHttpRequest object
AJAX主要特征:
- Make requests to the server without reloading the page
- Receive and work with data from the server
AJAX可与服务器交流并交换数据,无需重新刷新页面就能更新页面。
所谓异步,在这里简单地解释就是:向服务器发送请求的时候,我们不必等待结果,而是可以同时做其他的事情,等到有了结果它自己会根据设定进行后续操作,与此同时,页面是不会发生整页刷新的,提高了用户体验。
ajax api总体代码
function ajax (callback, requestMethod, url, isAsync, data) {
// create an instance of XMLHttpRequest
let xhr = createXHR()
// handle the server response
xhr.onreadystatechange = function handleRequest () {
// check the request's state
if (xhr.readyState === XMLHttpRequest.DONE) { // 4
// when the response's received
callback(xhr)
} else {
// not ready yet
}
}
// actually make the request
xhr.open(requestMethod, url, isAsync)
if (requestMethod === 'POST') {
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded')
xhr.send(data)
} else {
xhr.send()
}
}
function createXHR () {
if (typeof XMLHttpRequest !== 'undefined') { // IE7+ and other
return new XMLHttpRequest()
} else if (typeof ActiveXObject !== 'undefined') { // IE6 and older
// create the newest XHR object in MSXML
if (typeof createXHR.activeXString !== 'string') {
const versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp']
for (let i = 0, len = versions.length; i < len; i++) {
try {
new ActiveXObject(versions[i])
createXHR.activeXString = versions[i]
break
} catch (e) {}
}
}
return new ActiveXObject(createXHR.activeXString)
} else {
throw new Error('No XHR object available.')
}
}
复制代码
步骤:
-
创建XMLHttpRequest对象,也就是创建一个异步调用对象
// Old compatibility code, no longer needed. function createXHR () { if (typeof XMLHttpRequest !== 'undefined') { // IE7+ and other return new XMLHttpRequest() } else if (typeof ActiveXObject !== 'undefined') { // IE6 and older // create the newest XHR object in MSXML // if中的语句只需运行一次 // 第一次运行后,会在函数createXHR上创建一个属性activeXString // 当第二次运行时,只要该属性不被改写 // 便仍可使用 // 检验其是否存在,存在则直接跳过这段if if (typeof createXHR.activeXString !== 'string') { const versions = ['MSXML2.XMLHttp.6.0', 'MSXML2.XMLHttp.3.0', 'MSXML2.XMLHttp'] for (let i = 0, len = versions.length; i < len; i++) { // 检验创建ActiveXObject实例是否会报错 // 并不是真正地创建 try { new ActiveXObject(versions[i]) createXHR.activeXString = versions[i] break } catch (e) {} } } // 此处根据activeXString属性值,创建ActiveXObject实例 return new ActiveXObject(createXHR.activeXString) } else { throw new Error('No XHR object available.') } } 复制代码
-
创建一个新的HTTP请求,并指定该HTTP请求的方法、URL及验证信息
// actually make the request xhr.open(requestMethod, url, isAsync) 复制代码
-
设置响应HTTP请求状态变化的函数
XHR 对象的 readyState 属性,该属性表示请求/响应过程的当前活动阶段。这个属性可取的值如下:
- 0:未初始化。尚未调用 open()方法。
- 1:启动。已经调用 open()方法,但尚未调用 send()方法。
- 2:发送。已经调用 send()方法,但尚未接收到响应。
- 3:接收。已经接收到部分响应数据。
- 4:完成。已经接收到全部响应数据,而且已经可以在客户端使用了。
必须在调用
open()
之前指定onreadystatechange
事件处理程序才能确保跨浏览器兼容性。(因为open()
也会触发onreadystatechange
)// handle the server response xhr.onreadystatechange = function handleRequest () { // check the request's state if (xhr.readyState === XMLHttpRequest.DONE) { // 4 // when the response's received callback(xhr) } else { // not ready yet } } 复制代码
-
发送HTTP请求
// actually make the request if (requestMethod === 'POST') { xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded') xhr.send(data) } else { xhr.send() } 复制代码
-
获取异步调用返回的数据
用回调的方式传入api:
function callback (res) { // check the HTTP response status codes of the HTTP response if (res.status >= 200 && res.status < 300 || res.status === 304) { // perfect! console.log('success') ajxDisplay.innerText = res.responseText } else { // something wrong console.log('fail') } } 复制代码
-
使用JavaScript和DOM实现局部刷新
调用 ajax 封装函数。
btnAjax.onclick = function () { ajax(callback, 'GET', '/ajax', true) ajax(callback, 'POST', '/ajax', true, encodeURIComponent('name') + '=' + encodeURIComponent('hi')) } 复制代码
IE兼容性
参考:
- Msxml2.XMLHTTP和Microsoft.XMLHTTP有什么区别?
- best method of instantiating an xmlhttprequest object
- JavaScript: Which should I use, Microsoft.XMLHTTP or Msxml2.XMLHTTP?
- Using the right version of MSXML in Internet Explorer
看到有两种不同写法:
// MDN
new ActiveXObject('Microsoft.XMLHTTP')
// 高程三
new ActiveXObject('MSXML2.XMLHttp.6.0')
new ActiveXObject('MSXML2.XMLHttp.3.0')
new ActiveXObject('MSXML2.XMLHttp')
复制代码
- Msxml2.XMLHTTP是高版本,受msxml3.dll+支持
- Microsoft.XMLHTTP是低本,一般是msxml2.6以下版本使用
[[0], 1].reduce((p, v) => p.push([v])).toString()
的值
题目:
[[0], 1].reduce((p, v) => p.push([v])).toString() // "2"
复制代码
Why?
基础知识点:
-
arr.reduce(callback[, initialValue])
reduce返回值,是用的callback的返回值。
callback的四个参数:
- Accumulator (acc)
- Current Value (cur)
- Current Index (idx)
- Source Array (src)
-
push:
push的返回值,是数组的新长度。
-
函数返回值:
具体原因不清楚。
函数返回的数值,是number的包装对象,而非原始数据类型,所以能用
toString()
而不会报错。function foo () { return 1; } foo().__proto__; // Number {0, constructor: ƒ, toExponential: ƒ, toFixed: ƒ, toPrecision: ƒ, …} 1.__proto__; // Uncaught SyntaxError: Invalid or unexpected token 复制代码
题目分析:
- 默认没有initialValue的情况下,
p
的初始值为[0]
,v
为1
。 p.push([v])
,即p.push([1])
,于是p的现值为[0, [1]]
。- reduce返回的值 为
p.puh([1])
的返回值,即数组的新长度,也就是2。