1、JavaScript初识
1. 说几条 JavaScript 的基本规范?
(1)一个函数作用域中所有的变量声明应该尽量提到函数首部,用一个 var 声明,不允许出现两个连续的 var 声明,声明时 如果变量没有值,应该给该变量赋值对应类型的初始值,便于他人阅读代码时,能够一目了然的知道变量对应的类型值。
(2)代码中出现地址、时间等字符串时需要使用常量代替。
(3)在进行比较的时候吧,尽量使用'===', '!=='代替'==', '!='。
(4)不要在内置对象的原型上添加方法,如 Array, Date。
(5)switch 语句必须带有 default 分支。
(6)for 循环必须使用大括号。
(7)if 语句必须使用大括号。
2. JavaScript 代码中的 “use strict” 是什么意思?
use strict是一种ECMAscript 5 添加的(严格)运行模式,这种模式使得 Javascript 在更严格的条件下运行,
使JS编码更加规范化的模式,消除Javascript语法的一些不合理、不严谨之处,减少一些怪异行为。
默认支持的糟糕特性都会被禁用,比如不能用with,也不能在意外的情况下给全局变量赋值;
全局变量的显示声明,函数必须声明在顶层,不允许在非函数代码块内声明函数,arguments.callee也不允许使用;
消除代码运行的一些不安全之处,保证代码运行的安全,限制函数中的arguments修改,严格模式下的eval函数的行为和非严格模式的也不相同;
3. 说说严格模式的限制?
1,变量必须声明后再使用
2,函数的参数不能有同名属性,否则报错
3,不能使用with语句
4,不能对只读属性赋值,否则报错
5,不能使用前缀0表示八进制数,否则报错
6,不能删除变量delete prop,会报错,只能删除属性delete global[prop]
7,eval不会在它的外层作用域引入变量
8,eval和arguments不能被重新赋值
9,arguments不会自动反映函数参数的变化
10,不能使用arguments.callee
11,禁止this指向全局对象
12,不能使用fn.caller和fn.arguments获取函数调用的堆栈
13,增加了保留字(比如protected、static和interface)
参考文件:https://github.com/ecomfe/spec/blob/master/javascript-style-guide.md
2、运算符、运算符优先级
1. NaN 是什么?有什么特别之处?
NaN 属性是代表非数字值的特殊值。该属性用于指示某个值不是数字。可以把 Number 对象设置为该值,来指示其不是数字值。
提示:请使用 isNaN() 全局函数来判断一个值是否是 NaN 值。
Number.NaN 是一个特殊值,说明某些算术运算(如求负数的平方根)的结果不是数字。方法 parseInt() 和 parseFloat() 在不能解析指定的字符串时就返回这个值。对于一些常规情况下返回有效数字的函数,也可以采用这种方法,用Number.NaN 说明它的错误情况。
JavaScript 以 NaN 的形式输出 Number.NaN。请注意,NaN 与其他数值进行比较的结果总是不相等的,包括它自身在内。因此,不能与 Number.NaN 比较来检测一个值是不是数字,而只能调用 isNaN() 来比较。
2. == 与 === 有什么区别?
==:运算符称作相等,用来检测两个操作数是否相等,这里的相等定义的非常宽松,可以允许进行类型转换
===:用来检测两个操作数是否严格相等
1、对于string,number等基础类型,==和===是有区别的
不同类型间比较,==之比较“转化成同一类型后的值”看“值”是否相等,===如果类型不同,其结果就是不等
同类型比较,直接进行“值”比较,两者结果一样
2、对于Array,Object等高级类型,==和===是没有区别的
3、基础类型与高级类型,==和===是有区别的
对于==,将高级转化为基础类型,进行“值”比较,因为类型不同,===结果为false
3. console.log(1+"2") 和 console.log(1-"2") 的打印结果?
// 12 和 1-2
4. 为什么 console.log(0.2+0.1==0.3) 输出 false ?
在JavaScript中的二进制的浮点数0.2和0.6并不是十分精确,在他们相加的结果并非正好等于0.6,而是一个比较接近的数字 0.6000000000000001,所以条件判断结果为 false。
那么应该怎样来解决0.2+0.4等于0.6呢? 最好的方法是设置一个误差范围值,通常称为”机器精度“,而对于Javascript来说,这个值通常是2^-52,而在ES6中,已经为我们提供了这样一个
属性:Number.EPSILON,而这个值正等于2^-52。这个值非常非常小,在底层计算机已经帮我们运算好,并且无限接近0,但不等于0,。这个时候我们只要判断(0.2+0.4)-0.6小于Number.EPSILON,在这个误差的范围内就可以判定0.2+0.4===0.6为true。
5. 请用三元运算符(问号冒号表达式)改写以下代码:
if(a > 10) {
b = a
}else {
b = a - 2
}
//b = a > 10 ? a : a-2;
6. 以下代码输出的结果是?
var a = 1;
a+++a; //3
typeof a+2; //number2
7. 以下代码输出什么?
var d = a = 3, b = 4
console.log(d) //3
8. 以下代码输出什么?
var d = (a = 3, b = 4)
console.log(d) //4
9. 以下代码输出结果是?为什么?
var a = 1, b = 2, c = 3;
var val = typeof a + b || c >0 //typeof a 输出值为number ,
console.log(val) //number2
var d = 5;
var data = d ==5 && console.log('bb') // bb
console.log(data) //undefined
分析:1.因为按照运算符优先级,先算&&,
2.然后算 d==5是true
3.然后就继续 执行右侧 log
4.然后返回右侧 log 方法的return返回值
5.因为 log 没有返回值,所以使用函数默认的,默认的是 undefine所以data是 undefined。
var data2 = d = 0 || console.log('haha') //haha
console.log(data2) // undefined
解析:var data2 = d = 0 || console.log('haha')等同于var data2 = (d = 0) || [console.log('haha')],先给d赋值为0,进行判断时结果为false,所以要继续进行后面的判断,后面的判断同上题
var x = !!"Hello" + (!"world", !!"from here!!"); // true+true
console.log(x) //2
解析:var x = !!"Hello" + (!"world", !!"from here!!");等同于var x = 1 + (0, 1);,进行加法时会选择最后一个数字进行相加
10. 以下代码输出结果是?为什么?
var a = 1;
var b = 3;
console.log( a+++b ); //4
解析:a+++b等同于(a++)+b,在最终结果计算完之后再将a+1
11. 以下代码输出的结果是?为什么?
console.log(1+1); //2
console.log("2"+"4"); //24
console.log(2+"4"); // 24
console.log(+"4"); 4
3 、变量、值、数据类型、数据类型转换
1. JavaScript 定义了几种数据类型?哪些是原始类型?哪些是复杂类型?null 是对象吗?
原始数据类型 (不是对象且没有方法):Boolean、Null、Undefined、Number、String、Symbol(ES6 新增)、BigInt(ES10)
复杂数据类型:Object
Null 类型只有一个值"null"。
null 值表示一个空对象指针,所以使用typeof会返回"object";
undefined值是派生于null的,因此会有null == undefined //true。
null转为数字时,自动变成0。
null表示空值,即该处的值现在为空。调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。
无论在何种情况下都没必要把一个变量显式地设置为undefined,但是只要意在保存对象的变量还没有真正保存对象,就应该明确地让该变量保存null值。
null 有时会被当作一种对象类型,但是这其实只是语言本身的一个 bug,即对 null 执行typeof null 时会返回字符串 "object"。实际上,null 本身是基本类型
2. 对象类型和原始类型的不同之处?函数参数是对象会发生什么问题?
在 JS 中,除了原始类型那么其他的都是对象类型了。对象类型和原始类型不同的是,原始类型存储的是值,对象类型存储的是地址(指针)。当你创建了一个对象类型的时候,计算机会在内存中帮我们开辟一个空间来存放值,但是我们需要找到这个空间,这个空间会拥有一个地址(指针)。
const a=[]对于常量a来说,假设内存地址(指针)为#001,那么在地址#001的位置存放了值[],常量a存放了地址(指针)#001,再看以下代码
const a=[] ; const b= ab.push(1);
当我们将变量赋值给另外一个变量时,复制的是原本变量的地址(指针),也就是说当前变量b存放的地址(指针)也是#001,当我们进行数据修改的时候,就会修改存放在地址(指针)#001上的值,也就导致了两个变量的值都发生了改变。
function test(person) {
person.age = 26
person = {
name: 'yyy',
age: 30
}
return person
}
const p1 = {
name: 'yck',
age: 25
}
const p2 = test(p1)
console.log(p1) // -> ?
console.log(p2) // -> ?
对于以上代码,你是否能正确的写出结果呢?接下来让我为了解析一番:
首先,函数传参是传递对象指针的副本
到函数内部修改参数的属性这步,我相信大家都知道,当前p1的值也被修改了
但是当我们重新为了person分配了一个对象时就出现了分歧,请看下图
所以最后person拥有了一个新的地址(指针),也就和p1没有任何关系了,导致了最终两个变量的值是不相同的。
3. 怎样判断“值”属于哪种类型?typeof 是否能正确判断类型?instanceof 呢?instanceof 有什么作用?内部逻辑是如何实现的?
1、值类型的类型判断用typeof
// 值类型
console.log(typeof(x)); // undefined
console.log(typeof(10)); // number
console.log(typeof('abc')); // string
console.log(typeof(true)); // boolean
//虽然function也是一个引用类型对象,但是可以通过typeof判断:
var fn = function() {};
console.log(typeof fn); // function
如何判断null类型?用 ===:
console.log(null === null); // true
不能用 == ,因为:
console.log(null == undefined); // true
console.log(null === undefined); // false
2、引用类型的类型判断用instanceof
//引用类型
console.log(new String('string') instanceof String); // true
console.log(new Number(10) instanceof Number); // true
console.log(new Boolean(true) instanceof Boolean); // true
console.log(new Array(3,4,5) instanceof Array); // true
console.log([] instanceof Array); // true
var fn = function() {}
console.log(fn instanceof Function); // true
3、Object.prototype.toString.call( ) 方法
Object。prototype.toString.call()的实现原理
首先typeof 能够判断基本数据类型,但是除了null,typeof null 返回的是object
但是对于对象来说typeof不能准确判断类型,typeof 函数会返回function,除此之外全部都是object,不能准确判断类型
instanceof可以判断复杂数据类型,基本数据类型不可以
instanceof是通过原型链来判断的 ,A instanceof B,在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端(null,即Object.prototype._proto_),仍然不等于B,那么返回false,否则返回true
4. null,undefined 的区别?
null表示"没有对象",即该处不应该有值。典型用法是:
(1) 作为函数的参数,表示该函数的参数不是对象。
(2) 作为对象原型链的终点。
Object.getPrototypeOf(Object.prototype)// null
undefined表示"缺少值",就是此处应该有一个值,但是还没有定义。典型用法是:
(1)变量被声明了,但没有赋值时,就等于undefined。
(2) 调用函数时,应该提供的参数没有提供,该参数等于undefined。
(3)对象没有赋值的属性,该属性的值为undefined。
(4)函数没有返回值时,默认返回undefined。
var i;
i // undefined
function f(x){console.log(x)}
f() // undefined
var o = new Object();
o.p // undefined
var x = f();
x // undefined
5. 说一下 JS 中类型转换的规则?
6. 以下代码的输出?为什么?
console.log(a); //undefined
var a = 1;
console.log(b); //报异常 b is not defined
7. 以下代码输出什么?
var a = typeof 3+4
console.log(a) //number4
8. 以下代码输出什么?
var a = typeof typeof 4+4
console.log(a) //string4
分析:typeof 4 输出为"number";typeof "number"输出为"string"
4、流程控制语句
1. break 与 continue 有什么区别?
2. switch...case 语句中的 break 有什么作用?
3. for...of、 for...in 和 forEach、map 的区别?
4. 写出如下知识点的代码范例:
① if...else 的用法;
② switch...case 的用法;
③ while 的用法;
④ do...while 的用法;
⑤ for 遍历数组的用法;
⑥ for...in 遍历对象的用法;
⑦ break 和 continue 的用法。
5. 以下代码输出什么?
var a = 2
if(a = 1) {
console.log("a 等于 1")
}else {
console.log("a 不等于 1")
}
输出为:// a 等于 1
5、JS 函数
1. 写一个函数,返回参数的平方和?
function sumOfSquares() {
// 补全
let arr = Array.from(arguments)
return arr.reduce((result,item)=>{
return result += item*item
},0)
}
var result = sumOfSquares(2, 3, 4)
var result2 = sumOfSquares(1, 3)
console.log(result) // 29
console.log(result2) // 10
2. 如下代码的输出?为什么?
sayName("world");
sayAge(10);
function sayName(name) {
console.log("hello ", name);
}
var sayAge = function(age) {
console.log(age);
};
// 先输出:sayAge is not a function,然后再输出 hello world
3. 如下代码的输出?为什么?
var x = 10;
bar()
function bar() {
var x = 30;
function foo() {
console.log(x)
}
foo();
}
//输出:30
4. 如下代码的输出?为什么?
var x = 10
bar()
function foo() {
console.log(x)
}
function bar() {
var x = 30
foo()
}
输出: 10
5. 如下代码的输出?为什么?
var a = 1
function fn1() {
function fn3() {
function fn2() {
console.log(a)
}
fn2()
var a = 4
}
var a = 2
return fn3
}
var fn = fn1()
fn() // ?undefined
6. 如下代码的输出?为什么?
var a = 1
function fn1() {
function fn2() {
console.log(a)
}
function fn3() {
var a = 4
fn2()
}
var a = 2
return fn3
}
var fn = fn1()
fn() // 2
7. 如下代码的输出?为什么?
var a = 1
function fn1() {
function fn3() {
var a = 4
fn2()
}
var a = 2
return fn3
}
function fn2() {
console.log(a)
}
var fn = fn1()
fn() // 1
8. 如下代码的输出?为什么?
var a = 1
var c = {name: "oli", age: 2}
function f1(n) {
++n
}
function f2(obj) {
++obj.age
}
f1(a)
f2(c)
f1(c.age)
console.log(a)
console.log(c)
输出结果:
//1
//{age: 3,name: "oli"}
9. 如下代码的输出?为什么?
var obj1 = {a:1, b:2};
var obj2 = {a:1, b:2};
console.log(obj1 == obj2);
console.log(obj1 = obj2);
console.log(obj1 == obj2);
输出结果:
// false
//{a: 1,b: 2}
// true
5.1 嵌套函数、作用域和闭包
1. 闭包是什么?闭包的作用是什么?闭包有哪些使用场景?
1、变量作用域
要理解闭包,首先要理解javascript的特殊的变量作用域。
变量的作用域无非就两种:全局变量和局部变量。
javascript语言的特别之处就在于:函数内部可以直接读取全局变量,但是在函数外部无法读取函数内部的局部变量。
注意点:在函数内部声明变量的时候,一定要使用var命令。如果不用的话,你实际上声明的是一个全局变量!
2、如何从外部读取函数内部的局部变量?
出于种种原因,我们有时候需要获取到函数内部的局部变量。但是,上面已经说过了,正常情况下,这是办不到的!只有通过变通的方法才能实现。
那就是在函数内部,再定义一个函数。
function f1(){
var n=999;
function f2(){
alert(n); // 999
}
}
在上面的代码中,函数f2就被包括在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。
这就是Javascript语言特有的"链式作用域"结构(chain scope),
子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
既然f2可以读取f1中的局部变量,那么只要把f2作为返回值,我们不就可以在f1外部读取它的内部变量了吗!
3、闭包的概念
上面代码中的f2函数,就是闭包。
各种专业文献的闭包定义都非常抽象,我的理解是: 闭包就是能够读取其他函数内部变量的函数。
由于在javascript中,只有函数内部的子函数才能读取局部变量,所以说,闭包可以简单理解成“定义在一个函数内部的函数“。
所以,在本质上,闭包是将函数内部和函数外部连接起来的桥梁。
4、闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中,不会在f1调用后被自动清除。
为什么会这样呢?原因就在于f1是f2的父函数,而f2被赋给了一个全局变量,这导致f2始终在内存中,而f2的存在依赖于f1,因此f1也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。
这段代码中另一个值得注意的地方,就是"nAdd=function(){n+=1}"这一行,首先在nAdd前面没有使用var关键字,因此nAdd是一个全局变量,而不是局部变量。其次,nAdd的值是一个匿名函数(anonymous function),而这个匿名函数本身也是一个闭包,所以nAdd相当于是一个setter,可以在函数外部对函数内部的局部变量进行操作。
5、闭包的优点
(1)逻辑连续,当闭包作为另一个函数调用参数时,避免脱离当前逻辑而单独编写额外逻辑。
(2)方便调用上下文的局部变量。
(3)加强封装性,是第2点的延伸,可以达到对变量的保护作用。
6、使用闭包的注意点(缺点)
(1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。
(2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
7、闭包的特性
(1)作为函数变量的一个引用。当函数返回时,其处于激活状态。
(2)闭包就是当一个函数返回时,并没有释放资源的栈区。
8、闭包对页面的影响
通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅、更简洁的表达出代码;在某些方面提升代码的执行效率。
9、闭包的工作原理
因为闭包只有在被调用时才执行操作,所以它可以被用来定义控制结构。多个函数可以使用同一个环境,这使得他们可以通过改变那个环境相互交流。
10、使用场景
(1)采用函数引用方式的setTimeout调用。 例子
(2)将函数关联到对象的实例方法。
(3)封装相关的功能集。
2. 使用递归完成 1 到 100 的累加?
function add(i) {
if(i==1)
return i;
else
return i+add(i-1);
}
var s=add(100);
console.log(s) //5050
3. 谈谈垃圾回收机制的方式及内存管理?
回收机制方式
1、定义和用法:垃圾回收机制(GC:Garbage Collection),执行环境负责管理代码执行过程中使用的内存。
2、原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存。但是这个过程不是实时的,因为其开销比较大,所以垃圾回收器会按照固定的时间间隔周期性的执行。
3、实例如下:
function fn1() {
var obj = {name: ‘hanzichi’, age: 10};
}
function fn2() {
var obj = {name:‘hanzichi’, age: 10};
return obj;
}
var a = fn1();
var b = fn2();
fn1中定义的obj为局部变量,而当调用结束后,出了fn1的环境,那么该块内存会被js引擎中的垃圾回收器自动释放;在fn2被调用的过程中,返回的对象被全局变量b所指向,所以该块内存并不会被释放。
4、垃圾回收策略:标记清除(较为常用)和引用计数。
标记清除:
定义和用法:当变量进入环境时,将变量标记"进入环境",当变量离开环境时,标记为:“离开环境”。某一个时刻,垃圾回收器会过滤掉环境中的变量,以及被环境变量引用的变量,剩下的就是被视为准备回收的变量。
到目前为止,IE、Firefox、Opera、Chrome、Safari的js实现使用的都是标记清除的垃圾回收策略或类似的策略,只不过垃圾收集的时间间隔互不相同。
引用计数:
定义和用法:引用计数是跟踪记录每个值被引用的次数。
基本原理:就是变量的引用次数,被引用一次则加1,当这个引用计数为0时,被视为准备回收的对象。
内存管理
1、什么时候触发垃圾回收?
垃圾回收器周期性运行,如果分配的内存非常多,那么回收工作也会很艰巨,确定垃圾回收时间间隔就变成了一个值得思考的问题。
IE6的垃圾回收是根据内存分配量运行的,当环境中的变量,对象,字符串达到一定数量时触发垃圾回收。垃圾回收器一直处于工作状态,严重影响浏览器性能。
IE7中,垃圾回收器会根据内存分配量与程序占用内存的比例进行动态调整,开始回收工作。
2、合理的GC方案:(1)、遍历所有可访问的对象; (2)、回收已不可访问的对象。
3、GC缺陷:(1)、停止响应其他操作;
4、GC优化策略:(1)、分代回收(Generation GC);(2)、增量GC
4. 谈谈你对 JS 执行上下文栈和作用域链的理解?
一、JS执行上下文
执行上下文就是当前 JavaScript 代码被解析和执行时所在环境的抽象概念, JavaScript 中运行任何的代码都是在执行上下文中运行。
执行上下文类型分为:全局执行上下文和函数执行上下文。执行上下文创建过程中,需要做以下几件事:
(1)创建变量对象:首先初始化函数的参数arguments,提升函数声明和变量声明。
(2)创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。
(3)确定this的值,即 ResolveThisBinding
二、作用域
作用域就是变量和函数的可访问范围,控制这个变量或者函数可访问行和生命周期。
作用域有两种工作模型:词法作用域和动态作用域,JS采用的是词法作用域工作模型,词法作用域意味着作用域是由书写代码时变量和函数声明的位置决定的。( with 和 eval 能够修改词法作用域,但是不推荐使用,对此不做特别说明)
在 js 中是词法作用域,意思就是你的变量函数的作用域是由你的编码中的位置决定的,当然可以通过 apply、call、 bind 等函数进行修改。
在 ES6 之前,js 中的作用域分为两种:函数作用域和全局作用域,现在作用域分为:全局作用域、函数作用域、块级作用域。
全局作用域顾名思义,浏览器下就是 window ,作用域链的顶级就是它,那么只要不是被函数包裹的变量或者函数,它的作用域就是全局。
而函数作用域,就是在函数的体内声明的变量、函数及函数的参数,它们的作用域都是在这个函数内部。
三、JS执行上下文栈(后面简称执行栈)
执行栈,也叫做调用栈,具有 LIFO (后进先出) 结构,用于存储在代码执行期间创建的所有执行上下文。
规则如下:
首次运行JavaScript代码的时候,会创建一个全局执行的上下文并Push到当前的执行栈中,每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push当前执行栈的栈顶。
当栈顶的函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文的控制权将移动到当前执行栈的下一个执行上下文。
四、作用域链
我们知道函数在执行时是有个执行栈,在函数执行的时候会创建执行环境,也就是执行上下文,在上下文中有个大对象,保存执行环境定义的变量和函数,在使用变量的时候,就会访问这个大对象,这个对象会随着函数的调用而创建,函数执行结束出栈而销毁,那么这些大对象组成一个链,就是作用域链。那么函数内部未定义的变量,就会顺着作用域链向上查找,一直找到同名的属性。
5. 如下代码输出多少?如果想输出 3,那如何改造代码?
var fnArr = [];
for(var i=0; i<10; i++) {
fnArr[i] = function() {
return i
};
}
console.log(fnArr[3]())
//输出10
//如果想输出 3 方法一
var fnArr = [];
for (var i = 0; i < 10; i ++) {
(function(i){
fnArr[i] = function(){
return i
};
})(i)
}
console.log( fnArr[3]() )
//方法二
var fnArr = [];
for (var i = 0; i < 10; i ++) {
fnArr[i] = (function(j){
return function(){
return j
}
})(i)
}
console.log( fnArr[3]() )
//方法三
var fnArr = []
for (let i = 0; i < 10; i ++) {
fnArr[i] = function(){
return i
}
}
console.log( fnArr[3]() )
6. 封装一个 Car 对象。
var Car = (function() {
var speed = 0;
// 补充
return {
setSpeed: setSpeed,
getSpeed: getSpeed,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.setSpeed(30)
Car.getSpeed() // 30
Car.speedUp()
Car.getSpeed() // 31
Car.speedDown()
Car.getSpeed() // 30
实现:
var Car = (function(){
var speed = 0;
function setSpeed(val){
speed = val
return speed
}
function getSpeed(){
console.log(speed)
return speed
}
function speedUp(){
speed++
}
function speedDown(){
speed--
}
return {
setSpeed: setSpeed,
getSpeed: getSpeed,
speedUp: speedUp,
speedDown: speedDown
}
})()
Car.setSpeed(30)
Car.getSpeed() //30
Car.speedUp()
Car.getSpeed() //31
Car.speedDown()
Car.getSpeed() //30
7. 如下代码输出多少?如何连续输出 0, 1, 2, 3, 4?
for(var i=0; i<5; i++) {
setTimeout(function() {
console.log("delayer:" + i)
}, 0)
}
// 输出5个delayer:5
//改造 方法一
for(var i=0; i<5; i++){
(function(i){
setTimeout(function(){
console.log('delayer:' + i )
}, 0)
})(i)
}
//方法二
for(var i=0; i<5; i++){
setTimeout((function(i){
return function(){
console.log('delayer:' + i )
}
})(i), 0)
}
//方法三
for(let i=0; i<5; i++){
setTimeout(function(){
console.log('delayer:' + i )
}, 0)
}
8. 如下代码输出多少?
function makeCounter() {
var count = 0
return function() {
return count++
};
}
var counter = makeCounter()
var counter2 = makeCounter();
console.log(counter()) // 0
console.log(counter()) // 1
console.log(counter2()) // 0
console.log(counter2()) // 1
6、JS 数组
1. 写一个函数 squireArr,其参数是一个数组,作用是把数组中的每一项变为原值的平方。
var arr = [3, 4, 6]
function squireArr(arr) {
// 补全
window.arr = arr.map(function(a){return a*a})
}
squireArr(arr)
console.log(arr) // [9, 16, 36]
2. 写一个函数 squireArr,其参数是一个数组,返回一个新的数组,新数组中的每一项是原数组
对应值的平方,原数组不变。
var arr = [3, 4, 6]
function squireArr(arr) {
// 补全
return arr.map(function(a){returna*a});
}
var arr2 = squireArr(arr)
console.log(arr) // [3, 4, 6]
console.log(arr2) // [9, 16, 36]
3. 遍历 company 对象,输出里面每一项的值。
var company = {
name: "qdywxs",
age: 3,
sex: "男"
}
代码:
for(key in company) {
console.log(company[key]);
}
4. 遍历数组,打印数组里的每一项的平方。
var arr = [3, 4, 5]
arr.forEach((ite)=>{console.log(ite*ite)});
7、JS 对象
1. 介绍 JS 有哪些内置对象?
时间对象date,字符串对象string,数学对象Math,数值对象Number,数组对象Array,函数对象function, 正则表达式对象RegExp,函数参数集合arguments,布尔对象Boolean,错误对象Error,基础对象Object
2. 以下代码输出什么?
var name = "sex"
var company = {
name: "qdywxs",
age: 3,
sex: "男"
}
console.log(company[name]) //男
3. 以下代码输出什么?
var name = "sex"
var company = {
name: "qdywxs",
age: 3,
sex: "男"
}
console.log(company.name) // qdywxs
7.1 ES3 数组方法
1. 数组的哪些 API 会改变原数组?
修改原数组的API有:splice(),reverse(),fill(),copyWithin(),sort(),push(),pop(),unshift(),shift()
不修改原数组的有:slice(),map(),forEach(),every(),filter(),reduce(),entry(),entries(),find()
2. 写一个函数,操作数组,返回一个新数组,新数组中只包含正数。
function filterPositive(arr) {
// 补全
var newArr = []
for(var i = 0; i < arr.length; i++ ){
if (typeof arr[i] === 'number') {
if (arr[i] > 0) [
newArr.push(arr[i])
]
}
}
return newArr
}
var arr = [3, -1, 2, true]
filterPositive(arr)
console.log(filterPositive(arr)) // [3, 2]
3. 补全代码,实现数组按姓名、年纪、任意字段排序。
var users = [
{name: "John", age: 20, company: "Baidu"},
{name: "Pete", age: 18, company: "Alibaba"},
{name: "Ann", age: 19, company: "Tecent"}
]
function byField(field){
return function(user1, user2){
return user1[field] > user2[field]//因为field是字符串,所以不能用.来访问
}
}
users.sort(byField("age"))
users.sort(byField("company"))
4. 用 splice 函数分别实现 push、pop、shift、unshift 方法。
如:
function push(arr, value) {
arr.splice(arr.length, 0, value)
return arr.length
}
function pop(arr) {
return arr.splice(arr.length-1,1);
}
function shift(arr) {
return arr.splice(0,1);
}
function unshift(arr,value) {
return arr.splice(arr.length-1,1,value);
}
var arr = [3, 4, 5]
arr.push(10) // arr 变成 [3, 4, 5, 10],返回 4。
arr.pop(10) // arr 变成 [3, 4],返回 5。
arr.shift() //3
arr.unshift() //4
7.2 ES5 数组方法
1. for...of、 for...in 和 forEach、map 的区别?
1、for..in
for..in可以将JavaScript中的对象的属性依次循环出来,当for..in作用于数组时得到的是该元素的下标,且该下标是一个String对象而不是一个Number对象。(注意:for..in实际上是历史遗留问题,其遍历的实际上是对象的属性,之所以能够遍历数组,是因为数组实际上是一个对象,而其属性就是下标,所以使用for..in遍历数组得到的下标是String类型)
for..in的问题在于,当我们为一个数组添加了一个不是数字下标的属性时,遍历数组时会将这个属性当做是下标遍历出来:
let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x in a) {
console.log(x); // '0', '1', '2', 'name'
}
2、for..of
for..of就是为了遍历集合而专门设计的,它只遍历数组的元素:
let a = ['A', 'B', 'C'];
a.name = 'Hello';
for (let x of a) {
console.log(x); // 'A', 'B', 'C'
}
并且当for..of直接遍历对象时会报错:
let obj = {
name:'123123',
age:15
}
for (let cc of obj) {//Uncaught TypeError: obj is not iterable
console.log(cc)
}
for..of用于遍历对象需要使用Object.keys()才能正确遍历其属性(而Object.keys()方法实际上是将对象的属性键值以数组的对象返回了回来,实际上也是遍历数组。所以说for..of不能遍历对象其实也没有错):
let obj = {
name:'123123',
age:15
}
for (let cc of Object.keys(obj)) {
console.log(cc)//name,age
}
3、forEach
forEach是Iterable的内置方法,是一个高阶函数,其接受一个函数作为参数,每次迭代就回调该函数。是遍历Iterable(Array,Map,Set)最好的方式(但是相对于for..of有一个坏处就是不能通过break退出循环):
let arr = ['A', 'B', 'C'];
arr.forEach(function (element, index, array) {
// element: 指向当前元素的值
// index: 指向当前索引
// array: 指向Array对象本身
console.log(element + ', index = ' + index);
});
let set = new Set(['A', 'B', 'C']);
set.forEach(function (element, sameElement, set) {
//Set没有索引,因此回调函数的前两个参数都是元素本身
console.log(element);
});
let map = new Map([[1, 'x'], [2, 'y'], [3, 'z']]);
map.forEach(function (value, key, map) {
//map接受的参数为值,键和其自身
console.log(value);
});
2. 如何消除一个数组里面重复的元素?
1.兼容Set 和 Array.from() 的环境下:
let orderedArray = Array.from(new Set(myArray));
var myArray = ['a', 'b', 'a', 'b', 'c', 'e', 'e', 'c', 'd', 'd', 'd', 'd'];
var myOrderedArray = myArray.reduce(function (accumulator, currentValue) {
if (accumulator.indexOf(currentValue) === -1) {
accumulator.push(currentValue);
}
return accumulator
}, [])
console.log(myOrderedArray);
2.利用arr.reduce()去重
//简单去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre,cur)=>{
if(!pre.includes(cur)){
return pre.concat(cur)
}else{
return pre
}
},[])
console.log(newArr);// [1, 2, 3, 4]
3.扩展运算符数组去重
var arr = [1,2,3,4,5,2,3,1];
var set = new Set(arr);
var newArr = [...set ];
4.去重数组并排序
let arr = [1,2,1,2,3,5,4,5,3,4,4,4,4];
let result = arr.sort().reduce((init, current) => {
if(init.length === 0 || init[init.length-1] !== current) {
init.push(current);
}
return init;
}, []);
console.log(result); //[1,2,3,4,5]
3. 判断一个变量是否是数组,有哪些办法?
1、instanceof
function isArray (obj) {
return obj instanceof Array;
}
2、Array对象的 isArray方法
function isArray (obj) {
return Array.isArray(obj);
}
3、Object.prototype.toString
function isArray (obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
4. ["1", "2", "3"].map(parseInt) 答案是多少?
输出:[ 1,NaN,NaN ]
解析:
map方法
引入MDN的解释,map()方法创建一个新数组,其结果是该数组中的每个元素都调用一个提供的函数后返回的结果。
var arr = [1, 2, 3];
var arr1 = arr.map(x => x+1);
console.log(arr1); // 2,3,4
可以看出map方法中接受一个函数function,用来处理遍历数组中的每一个元素。
new_array = [].map(function callback(currentValue[,index[,array]]){ // Return element for new_array }[, thisArg]);
这个callback一共可以接收三个参数,其中第一个参数代表当前被处理的元素,第二个参数代表该元素的索引。
parseInt函数
parseInt是用来解析字符串,使字符串成为指定基数的整数,parseInt的基本语法,
parseInt(string, radix)
接收两个参数,第一个表示被处理的值(字符串),第二个表示为解析时的基数。radix是一个介于2-36之间的整数,返回解析后的整数值。 如果被解析参数的第一个字符无法被转化成数值类型,则返回 NaN
praseInt('111') // 111
parseInt('111', 0) // 111
// radix为0时,且string参数不以“0x”和“0”开头时,按照10为基数处理
parseInt('111', 1) // NaN 【2 <= radix <= 36】
parseInt('111', 2) // 7
从上面可以看出根据指定不同的radix,返回不同的值。
radix参数为n将会把第一个参数看作是一个数的n进制表示,而返回的值则是十进制。
parseInt('123', 5) // 将'123'看作5进制数,返回十进制数38 => 1*5^2 + 2*5^1 + 3*5^0 = 38
下面我们来分析一下[‘1’, ‘2’, ‘3’].map(parseInt);
parseInt(‘1’, 0); // radix为0时,使用默认的10进制。
parseInt(‘2’, 1); // radix值在2-36,无法解析,返回NaN
parseInt(‘3’, 2); // 基数为2,2进制数表示的数中,最大值小于3,无法解析,返回NaN
map函数返回的是一个数组,所以最后结果为[1, NaN, NaN]。
5. 取数组的最大值(ES5、ES6)?
1.apply()应用某一对象的一个方法,用另一个对象替换当前对象
var max = Math.max.apply(null,arr);
console.log(max)
由于max()里面参数不能为数组,所以借助apply(funtion,args)方法调用Math.max(),function为要调用的方法,args是数组对象,当function为null时,默认为上文,即相当于apply(Math.max,arr)
2.call()调用一个对象的一个方法,以另一个对象替换当前对象
var max1 = Math.max.call(null,7,2,0,-3,5)
console.log(max1)
call()与apply()类似,区别是传入参数的方式不同,apply()参数是一个对象和一个数组类型的对象,call()参数是一个对象和参数列表
3.sort()+reverse()
//sort()排序默认为升序,reverse()将数组掉个
var max3 = arr.sort().reverse()[0];
console.log(max3)
4.sort()
//b-a从大到小,a-b从小到大
var max2 = arr.sort(function(a,b){
return b-a;
})[0];
console.log(max2);
5.es6扩展语法
var arr = [1, 2, 3];
var max = Math.max(...arr);
6. 实现一个 reduce 函数,作用和原生的 reduce 类似下面的例子。
Ex:
var sum = reduce([1, 2, 3], function(memo, num) {return memo + num;}, 0); => 6
如下:
function newReduce(arr, iteratee, initValue){
var tmpArr = (initValue === undefined ? [] : [initValue]).concat(arr);//判断是否有原始值
while(tmpArr.length > 1){
tmpArr.splice(0, 2, iteratee(tmpArr[0], tmpArr[1]));
}
return tmpArr[0];
}
var sum = newReduce([1, 2, 3], function(memo, num){ return memo + num; }, 0);
console.log(sum);
7. 怎样用原生 JS 将一个多维数组拍平?
方法一:concat
function flatten(arr) {
var result = [];
arr.forEach(function(value, index, array) {
if(Array.isArray(value)) {
result = result.concat(flatten(value));
} else {
result = result.concat(value);
}
});
return result;
}
方法二:闭包
function flatten(arr) {
var result = [];
function _flatten(arr) {
arr.forEach(function(value, index, array){
if(Array.isArray(value)) {
_flatten(value);
} else {
result.push(value);
}
});
}
_flatten(arr);
return result;
}
方法三:reduce
function flatten(arr) {
return arr.reduce(function(a, b){
if(Array.isArray(b)) {
return a.concat(flatten(b));
} else {
return a.concat(b);
}
},[]);
}
function newFlatten(arr){
return arr.reduce(function(initArr, currentArr){
return initArr.concat(Array.isArray(currentArr) ? newFlatten(currentArr) : [currentArr]);
} ,[]);
}
var result = newFlatten([1, [2], [3, [[4]]]]);
console.log(result);
console.log(flat(array)) // [1, 2, 3, 4, 5]
8、面向对象编程
8.1 对象构造函数
1. new 的原理是什么?通过 new 的方式创建对象和通过字面量创建有什么区别?
new的原理:
创建一个新对象。
这个新对象会被执行[[原型]]连接。
将构造函数的作用域赋值给新对象,即this指向这个新对象.
如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。
function new(func) {
lat target = {};
target.__proto__ = func.prototype;
let res = func.call(target);
if (typeof(res) == "object" || typeof(res) == "function") {
return res;
}
return target;
}
字面量创建对象,不会调用 Object构造函数, 简洁且性能更好;
new Object() 方式创建对象本质上是方法调用,涉及到在proto链中遍历该方法,当找到该方法后,又会生产方法调用必须的 堆栈信息,方法调用结束后,还要释放该堆栈,性能不如字面量的方式。
通过对象字面量定义对象时,不会调用Object构造函数。
2. Object.create 有什么作用?
语法:
Object.create(proto, [propertiesObject])
//方法创建一个新对象,使用现有的对象来提供新创建的对象的proto。
参数:
proto : 必须。表示新建对象的原型对象,即该参数会被赋值到目标对象(即新对象,或说是最后返回的对象)的原型上。该参数可以是null,对象, 函数的prototype属性(创建空的对象时需传null , 否则会抛出TypeError异常)。
propertiesObject : 可选。 添加到新创建对象的可枚举属性(即其自身的属性,而不是原型链上的枚举属性)对象的属性描述符以及相应的属性名称。这些属性对应Object.defineProperties()的第二个参数。
3 返回值:
在指定原型对象上添加新属性后的对象。
案例说明:
1)创建对象的方式不同
new Object() 通过构造函数来创建对象, 添加的属性是在自身实例下。
Object.create() es6创建对象的另一种方式,可以理解为继承一个对象, 添加的属性是在原型下。
// new Object() 方式创建
var a = { rep : 'apple' }
var b = new Object(a)
console.log(b) // {rep: "apple"}
console.log(b.__proto__) // {}
console.log(b.rep) // {rep: "apple"}
// Object.create() 方式创建
var a = { rep: 'apple' }
var b = Object.create(a)
console.log(b) // {}
console.log(b.__proto__) // {rep: "apple"}
console.log(b.rep) // {rep: "apple"}
Object.create()方法创建的对象时,属性是在原型下面的,也可以直接访问 b.rep // {rep: "apple"} ,
此时这个值不是吧b自身的,是它通过原型链proto来访问到b的值。
2)创建对象属性的性质不同
// 创建一个以另一个空对象为原型,且拥有一个属性p的对象
o = Object.create({}, { p: { value: 42 } })
// 省略了的属性特性默认为false,所以属性p是不可写,不可枚举,不可配置的:
o.p = 24
o.p
//42
o.q = 12
for (var prop in o) {
console.log(prop)
}
//"q"
delete o.p
//false
Object.create() 用第二个参数来创建非空对象的属性描述符默认是为false的,而构造函数或字面量方法创建的对象属性的描述符默认为true。
3)创建空对象时不同
当用构造函数或对象字面量方法创建空对象时,对象时有原型属性的,即有_proto_;
当用Object.create()方法创建空对象时,对象是没有原型属性的。
3. 怎样判断“值”属于哪种数据类型?typeof 是否能正确判断类型?instanceof 呢?
首先typeof 能够判断基本数据类型,但是除了null,typeof null 返回的是object
但是对于对象来说typeof不能准确判断类型,typeof 函数会返回function,除此之外全部都是object,不能准确判断类型
instanceof可以判断复杂数据类型,基本数据类型不可以,instanceof是通过原型链来判断的 ,A instanceof B,在A的原型链中层层查找,是否有原型等于B.prototype,如果一直找到A的原型链的顶端(null,即Object.prototype._proto_),仍然不等于B,那么返回false,否则返回true
4. instanceof 有什么作用?内部逻辑是如何实现的?
nstanceOf判断一个对象是不是某个类型的实例
[1, 2, 3] instanceof Array; //true
可以看到[1, 2, 3]是类型Array的实例
[1, 2, 3] instanceof Object; //true
5.JavaScript 有哪些方法定义对象?
方式一:
通过对象字面量表示法(又称为直接量、原始方式)。
var obj = {name:"moyu"};
方式二:
通过new和构造函数Object()、String()等。
var obj = new Object();
方式三:
自定义一个对象的构造函数,然后实例化对象。
function a(o){
this.name = "moyu"
}
var obj = new a();
方式四:
通过Object.create()
var o1 = Object.create({x:1, y:2}); // o1继承了属性x和y
6. 如下代码中?new 一个函数本质上做了什么?
function Modal(msg) {
this.msg = msg
}
var modal = new Modal()
1.执行 new Modal()
1.1创建一个空对象 {},假设名字是 tmpObj
1.2执行 Modal 函数,执行过程中对 this 操作就是对 tmpObj 进行操作
1.3 函数执行完后返回刚刚创建的 tmpObj
2.把 tmpObj 赋值给 modal (modal指向同一个对象)
8.2 使用原型
1. JS 原型是什么?如何理解原型链?
一、什么是原型
原型是Javascript中的继承的基础,JavaScript的继承就是基于原型的继承。
1.1 函数的原型对象
在JavaScript中,我们创建一个函数A(就是声明一个函数), 那么浏览器就会在内存中创建一个对象B,而且每个函数都默认会有一个属性 prototype 指向了这个对象( 即:prototype的属性的值是这个对象 )。这个对象B就是函数A的原型对象,简称函数的原型。这个原型对象B 默认会有一个属性 constructor 指向了这个函数A ( 意思就是说:constructor属性的值是函数A )。
看下面的代码:
/*
声明一个函数,则这个函数默认会有一个属性叫 prototype 。而且浏览器会自动按照一定的规则
创建一个对象,这个对象就是这个函数的原型对象,prototype属性指向这个原型对象。这个原型对象
有一个属性叫constructor 执行了这个函数
注意:原型对象默认只有属性:constructor。其他都是从Object继承而来,暂且不用考虑。
*/
function Person () {
}
下面的图描述了声明一个函数之后发生的事情:
1.2 使用构造函数创建对象
当把一个函数作为构造函数 (理论上任何函数都可以作为构造函数) 使用new创建对象的时候,那么这个对象就会存在一个默认的不可见的属性,来指向了构造函数的原型对象。 这个不可见的属性我们一般用 [[prototype]] 来表示,只是这个属性没有办法直接访问到。
看下面的代码:
function Person () {
}
/*
利用构造函数创建一个对象,则这个对象会自动添加一个不可见的属性 [[prototype]], 而且这个属性
指向了构造函数的原型对象。
*/
var p1 = new Person();
观察下面的示意图:
说明:
从上面的图示中可以看到,创建p1对象虽然使用的是Person构造函数,但是对象创建出来之后,这个p1对象其实已经与Person构造函数没有任何关系了,p1对象的[[ prototype ]]属性指向的是Person构造函数的原型对象。
如果使用new Person()创建多个对象,则多个对象都会同时指向Person构造函数的原型对象。
我们可以手动给这个原型对象添加属性和方法,那么p1,p2,p3…这些对象就会共享这些在原型中添加的属性和方法。
如果我们访问p1中的一个属性name,如果在p1对象中找到,则直接返回。如果p1对象中没有找到,则直接去p1对象的[[prototype]]属性指向的原型对象中查找,如果查找到则返回。(如果原型中也没有找到,则继续向上找原型的原型—原型链。 后面再讲)。
如果通过p1对象添加了一个属性name,则p1对象来说就屏蔽了原型中的属性name。 换句话说:在p1中就没有办法访问到原型的属性name了。
通过p1对象只能读取原型中的属性name的值,而不能修改原型中的属性name的值。 p1.name = “李四”; 并不是修改了原型中的值,而是在p1对象中给添加了一个属性name。
看下面的代码:
function Person () {
}
// 可以使用Person.prototype 直接访问到原型对象
//给Person函数的原型对象中添加一个属性 name并且值是 "张三"
Person.prototype.name = "张三";
Person.prototype.age = 20;
var p1 = new Person();
/*
访问p1对象的属性name,虽然在p1对象中我们并没有明确的添加属性name,但是
p1的 [[prototype]] 属性指向的原型中有name属性,所以这个地方可以访问到属性name
就值。
注意:这个时候不能通过p1对象删除name属性,因为只能删除在p1中删除的对象。
*/
alert(p1.name); // 张三
var p2 = new Person();
alert(p2.name); // 张三 都是从原型中找到的,所以一样。
alert(p1.name === p2.name); // true
// 由于不能修改原型中的值,则这种方法就直接在p1中添加了一个新的属性name,然后在p1中无法再访问到
//原型中的属性。
p1.name = "李四";
alert("p1:" + p1.name);
// 由于p2中没有name属性,则对p2来说仍然是访问的原型中的属性。
alert("p2:" + p2.name); // 张三
二、与原型有关的几个属性和方法
2.1 prototype属性
prototype 存在于构造函数中 (其实任意函数中都有,只是不是构造函数的时候prototype我们不关注而已) ,他指向了这个构造函数的原型对象。
参考前面的示意图。
2.2 constructor属性
constructor属性存在于原型对象中,他指向了构造函数
看下面的代码:
function Person () {
}
alert(Person.prototype.constructor === Person); // true
var p1 = new Person();
//使用instanceof 操作符可以判断一个对象的类型。
//typeof一般用来获取简单类型和函数。而引用类型一般使用instanceof,因为引用类型用typeof 总是返回object。
alert(p1 instanceof Person); // true
我们根据需要,可以Person.prototype 属性指定新的对象,来作为Person的原型对象。
但是这个时候有个问题,新的对象的constructor属性则不再指向Person构造函数了。
看下面的代码:
function Person () {
}
//直接给Person的原型指定对象字面量。则这个对象的constructor属性不再指向Person函数
Person.prototype = {
name:"志玲",
age:20
};
var p1 = new Person();
alert(p1.name); // 志玲
alert(p1 instanceof Person); // true
alert(Person.prototype.constructor === Person); //false
//如果constructor对你很重要,你应该在Person.prototype中添加一行这样的代码:
/*
Person.prototype = {
constructor : Person //让constructor重新指向Person函数
}
*/
2.3 __proto__ 属性(注意:左右各是2个下划线)
用构造方法创建一个新的对象之后,这个对象中默认会有一个不可访问的属性 [[prototype]] , 这个属性就指向了构造方法的原型对象。
但是在个别浏览器中,也提供了对这个属性[[prototype]]的访问(chrome浏览器和火狐浏览器。ie浏览器不支持)。访问方式:p1.__proto__
但是开发者尽量不要用这种方式去访问,因为操作不慎会改变这个对象的继承原型链。
function Person () {
}
//直接给Person的原型指定对象字面量。则这个对象的constructor属性不再指向Person函数
Person.prototype = {
constructor : Person,
name:"志玲",
age:20
};
var p1 = new Person();
alert(p1.__proto__ === Person.prototype); //true
2.4 hasOwnProperty() 方法
大家知道,我们用去访问一个对象的属性的时候,这个属性既有可能来自对象本身,也有可能来自这个对象的[[prototype]]属性指向的原型。
那么如何判断这个对象的来源呢?
hasOwnProperty方法,可以判断一个属性是否来自对象本身。
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
//sex属性是直接在p1属性中添加,所以是true
alert("sex属性是对象本身的:" + p1.hasOwnProperty("sex"));
// name属性是在原型中添加的,所以是false
alert("name属性是对象本身的:" + p1.hasOwnProperty("name"));
// age 属性不存在,所以也是false
alert("age属性是存在于对象本身:" + p1.hasOwnProperty("age"));
所以,通过hasOwnProperty这个方法可以判断一个对象是否在对象本身添加的,但是不能判断是否存在于原型中,因为有可能这个属性不存在。
也即是说,在原型中的属性和不存在的属性都会返回fasle。
如何判断一个属性是否存在于原型中呢?
2.5 in 操作符
in操作符用来判断一个属性是否存在于这个对象中。但是在查找这个属性时候,现在对象本身中找,如果对象找不到再去原型中找。换句话说,只要对象和原型中有一个地方存在这个属性,就返回true
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
alert("sex" in p1); // 对象本身添加的,所以true
alert("name" in p1); //原型中存在,所以true
alert("age" in p1); //对象和原型中都不存在,所以false
回到前面的问题,如果判断一个属性是否存在于原型中:
如果一个属性存在,但是没有在对象本身中,则一定存在于原型中。
function Person () {
}
Person.prototype.name = "志玲";
var p1 = new Person();
p1.sex = "女";
//定义一个函数去判断原型所在的位置
function propertyLocation(obj, prop){
if(!(prop in obj)){
alert(prop + "属性不存在");
}else if(obj.hasOwnProperty(prop)){
alert(prop + "属性存在于对象中");
}else {
alert(prop + "对象存在于原型中");
}
}
propertyLocation(p1, "age");
propertyLocation(p1, "name");
propertyLocation(p1, "sex");
三、组合原型模型和构造函数模型创建对象
3.1 原型模型创建对象的缺陷
原型中的所有的属性都是共享的。也就是说,用同一个构造函数创建的对象去访问原型中的属性的时候,大家都是访问的同一个对象,如果一个对象对原型的属性进行了修改,则会反映到所有的对象上面。
但是在实际使用中,每个对象的属性一般是不同的。张三的姓名是张三,李四的姓名是李四。
**但是,这个共享特性对 方法(属性值是函数的属性)又是非常合适的。**所有的对象共享方法是最佳状态。这种特性在c#和Java中是天生存在的。
3.2 构造函数模型创建对象的缺陷
在构造函数中添加的属性和方法,每个对象都有自己独有的一份,大家不会共享。这个特性对属性比较合适,但是对方法又不太合适。因为对所有对象来说,他们的方法应该是一份就够了,没有必要每人一份,造成内存的浪费和性能的低下。
function Person() {
this.name = "李四";
this.age = 20;
this.eat = function() {
alert("吃完东西");
}
}
var p1 = new Person();
var p2 = new Person();
//每个对象都会有不同的方法
alert(p1.eat === p2.eat); //fasle
可以使用下面的方法解决:
function Person() {
this.name = "李四";
this.age = 20;
this.eat = eat;
}
function eat() {
alert("吃完东西");
}
var p1 = new Person();
var p2 = new Person();
//因为eat属性都是赋值的同一个函数,所以是true
alert(p1.eat === p2.eat); //true
但是上面的这种解决方法具有致命的缺陷:封装性太差。使用面向对象,目的之一就是封装代码,这个时候为了性能又要把代码抽出对象之外,这是反人类的设计。
3.3 使用组合模式解决上述两种缺陷
原型模式适合封装方法,构造函数模式适合封装属性,综合两种模式的优点就有了组合模式。
//在构造方法内部封装属性
function Person(name, age) {
this.name = name;
this.age = age;
}
//在原型对象内封装方法
Person.prototype.eat = function (food) {
alert(this.name + "爱吃" + food);
}
Person.prototype.play = function (playName) {
alert(this.name + "爱玩" + playName);
}
var p1 = new Person("李四", 20);
var p2 = new Person("张三", 30);
p1.eat("苹果");
p2.eat("香蕉");
p1.play("志玲");
p2.play("凤姐");
四、动态原型模式创建对象
前面讲到的组合模式,也并非完美无缺,有一点也是感觉不是很完美。把构造方法和原型分开写,总让人感觉不舒服,应该想办法把构造方法和原型封装在一起,所以就有了动态原型模式。
动态原型模式把所有的属性和方法都封装在构造方法中,而仅仅在需要的时候才去在构造方法中初始化原型,又保持了同时使用构造函数和原型的优点。
看下面的代码:
//构造方法内部封装属性
function Person(name, age) {
//每个对象都添加自己的属性
this.name = name;
this.age = age;
/*
判断this.eat这个属性是不是function,如果不是function则证明是第一次创建对象,
则把这个funcion添加到原型中。
如果是function,则代表原型中已经有了这个方法,则不需要再添加。
perfect!完美解决了性能和代码的封装问题。
*/
if(typeof this.eat !== "function"){
Person.prototype.eat = function () {
alert(this.name + " 在吃");
}
}
}
var p1 = new Person("志玲", 40);
p1.eat();
说明:
组合模式和动态原型模式是JavaScript中使用比较多的两种创建对象的方式。
建议以后使用动态原型模式。他解决了组合模式的封装不彻底的缺点。
原文链接:https://blog.csdn.net/u012468376/java/article/details/53121081
五、原型链
每个实例对象( object )都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象(prototype )。该原型对象也有一个自己的原型对象( __proto__ ) ,层层向上直到一个对象的原型对象为null。根据定义,null没有原型,并作为这个原型链中的最后一个环节。
2. JS 如何实现继承?
一、原型链实现继承
原型链实现继承的思想:利用原型让一个引用类型继承另一个引用类型的属性和方法。
原型链的基本概念: 当一个原型对象等于另一个类型的实例,此时的原型对象将包含一个指向另一个指向另一个原型的指针。同时,另一个原型中也包含着一个指向另一个构造函数的指针。如果另一个原型是另一个类型的实例,此时实例和原型就构成了原型链
function SuperType(){
this.property=true;
}
SuperType.prototype.getSuperValue=function(){
returnthis.property;
}
function SubType(){
this.subproperty=false;
}//通过创建SuperType的实例继承了SuperTypeSubType.prototype=new SuperType();SubType.prototype.getSubValue=function(){
returnthis.subproperty;
}
var instance=new SubType();
alert(instance.getSuperValue()); //true
对于代码的解释:
首先定义了两个类型SuperType和SubType。此时SubType通过创建SuperType的实例(new SuperType()),并将该实例(new SuperType())赋给了SubType的原型SubType.prototype的方式 继承了 SuperType。
此时存在于SuperType中实例中的所有属性和方法,也存在于SubType.prototype中。此时,实例、原型和构造函数之间的关系如下图所示。
SubType的实例instance指向SubType的原型SubType.prototype,SubType.prototype又指向SuperType的原型。
注意:getSuperValue()方法仍然还在SuperType.prototype中,但property则位于SubType.prototype中。这是因为property是一个实例属性,而getSuperValue()则是一个原型方法。
原型链存在的问题:
1)包含引用类型值的原型属性会被所有实例共享,这会导致对一个实例的修改会影响另一个实例。在通过原型来实现继承时,原型实际上会变成另一个类型的实例。原先的实例属性就变成了现在的原型属性
(2)在创建子类型的实例时,不能向超类型的构造函数中传递参数
二、借用构造函数实现继承
借用构造函数的基本思想,即在子类型构造函数的内部调用超类型构造函数。函数只不过是在特定环境中执行代码的对象,因此通过使用apply()和call()方法可以在新创建的对象上执行构造函数。
function SuperType(){
this.colors=["red", "blue", "green"];
}function SubType(){
//继承SuperType SuperType.call(this);}varinstance1=new SubType();
instance1.colors.push("black");
alert(instance1.colors); //red,bllue,green,blackvarinstance2=new SubType();
alert(instance2.colors); //red,blue,green
代码的解释:
SuperType.call(this);“借调”了超类型的构造函数。通过使用call()方法(或者apply()方法),在新创建的SubType实例的环境下调用了SuperType构造函数。这样一来就会在新的SubType对象上执行SuperType()函数中定义的所有对象初始化代码。结果,SubType的每个实例都会有自己的colors属性副本
借用构造函数的优势:可以在子类型构造函数中向超类型构造函数传递参数
function SuperType(name){
this.name=name;
}function SubType(){
//继承了SuperType,同时还传递了参数SuperType.call(this,"mary");//this(SubType的实例)调用SuperType构造函数
//实例属性this.age=22;
}varinstance=new SubType();
alert(instance.name); //maryalert(instance.age);//29
借用构造函数的问题:
1)无法避免构造函数模式存在的问题,方法都在构造函数中定义,因此无法复用函数。
2)在超类型的原型中定义的方法,对子类型而言是不可见的。因此这种技术很少单独使用。
三、组合继承
组合继承:指的是将原型链和借用构造函数的技术组合到一起。思路是使用原型链实现对原型方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数的复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){
this.name=name;
this.colors=["red", "blue", "green"];
}
SuperType.prototype.sayName=function(){
alert(this.name);
};function SubType(name, age){
//继承属性 使用借用构造函数实现对实例属性的继承 SuperType.call(this,name);
this.age=age;
}//继承方法 使用原型链实现SubType.prototype=new SuperType();SubType.prototype.constructor=SubType;
subType.prototype.sayAge=function(){
alert(this.age);
};varinstance1=newSubType("mary", 22);
instance1.colors.push("black");
alert(instance1.colors); //red,blue,green,blackinstance1.sayName();//maryinstance1.sayAge();//22varinstance2=newSubType("greg", 25);
alert(instance2.colors); //red,blue,greeninstance2.sayName();//greginstance2.sayAge();//25
代码解释:
SuperType构造函数定义了两个属性:name和colors。SuperType的原型定义了一个方法sayName()。
SubType构造函数在调用SuperType构造函数时传入了name参数。并定义了自己的属性age。
然后,将SuperType实例赋值给SubType的原型,然后又在新原型上定义了sayAge方法。
此时可以让两个不同的SubType实例分别拥有自己的属性,又可以使用相同的方法
组合继承的优势:
避免了原型链和借用构造函数的缺点,融合了他们的优点,是JavaScript中最常用的继承模式。instanceof和isprototypeOf()也能够用于识别基于组合继承创建的对象
四、原型式继承
五、寄生式继承
六、寄生组合式继承
3. 实现一个函数 clone 可以对 JavaScript 中的五种主要数据类型(Number、string、Object、Array、
Boolean)进行复制?
function clone(obj) {
var o;
switch (typeof obj) {
case "undefined":
break;
case "string":
o = obj + "";
break;
case "number":
o = obj - 0;
break;
case "boolean":
o = obj;
break;
case "object": // object 分为两种情况 对象(Object)或数组(Array)
if (obj === null) {
o = null;
} else {
if (Object.prototype.toString.call(obj).slice(8, -1) === "Array") {
o = [];
for (var i = 0; i < obj.length; i++) {
o.push(clone(obj[i]));
}
} else {
o = {};
for (var k in obj) {
o[k] = clone(obj[k]);
}
}
}
break;
default:
o = obj;
break;
}
return o;
}
4. 对 String 做扩展,实现如下方式获取字符串中频率最高的字符:
var str = 'ahbbccdeddddfg';
//补充方法
String.prototype.getMostOften = function(){
var obj = {}
for(var i = 0; i
if(!obj[letter]){
obj[letter] = 1
}else{
obj[letter]++
}
}
console.log(obj)
var maxIdx = 0
var maxLetter = ''
for(var key in obj){
if(maxIdx < obj[key]){
maxIdx = obj[key]
maxLetter = key
}
}
return maxLetter
}
//
var ch = str.getMostOften()
console.log(ch) // d,因为 d 出现了 5 次
5. 有如下代码,代码中并未添加 toString 方法,这个方法是哪里来的?画出原型链图进行解释:
function People() {
}
var p = new People()
p.toString()
分析如下:
记当前对象为p,查找p属性、方法
没有找到,通过p的proto属性,找到其类型People的prototype属性(记为prop)继续查找
没有找到,把prop记为obj做递归重复步骤一,通过类似方法找到prop的类型Object的 prototype进行查找,直至找到toString方法
6. 有如下代码,解释 Person、 prototype、__proto__、p、constructor 之间的关联:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log("My name is :" + this.name);
}
var p = new Person("Oli")
p.sayName();
分析:
1.我们通过函数定义了类Person,类(函数)Person自动获得属性prototype
2.Person是一个构造函数,构造函数构造出实例 p,
3.p是构造函数Person的一个实例,p的 proto 指向了Person的prototype属性,
4.prototype是构造函数内部的原型对象,所以拥有contructor和proto属性,其中contructor属性指向构造函数Person,proto指向该对象的原型.
7. 下面两种写法有什么区别?
// 方法一:
function People(name, sex) {
this.name = name;
this.sex = sex;
this.printName = function() {
console.log(this.name);
}
}
var p1 = new People("Oli", 2)
// 方法二:
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
Person.prototype.printName = function() {
console.log(this.name);
}
var p1 = new Person("Aman", 2);
方法一是直接将方法放在自己的私有空间内,没有放在原型链上,并没有起到公共代码的作用
方法二通过将方法放在原型链上,起到了公共代码的作用,节省了代码量,提升了性能。
8. 补全代码,实现继承:
function Person(name, sex){
// 补全
this.name=name; // todo ...
this.sex=sex
};
Person.prototype.getName = function() {
// 补全
console.log(this.name)// todo ...
};
function Male(name, sex, age) {
// 补全
Person.call(this,name,sex)
this.age=age //todo ...
};
// 补全
Male.prototype=Object.create(Person.prototype);//todo ...
Male.prototype.constructor=Male
Male.prototype.getAge = function() {
// 补全
onsole.log(this.age)//todo ...
};
var catcher = new Male("Oli", "男", 2);
catcher.getName();
9. 如下代码中 call 的作用是什么?
function Person(name, sex) {
this.name = name;
this.sex = sex;
}
function Male(name, sex, age) {
Person.call(this, name, sex); // 这里的 call 有什么作用?
this.age = age;
}
call调用了Person构造函数,将Person里面的this替换成指向Male构造函数的对象
10.判断字符串是否同构:如果 *__s __*中的字符可以被替换得到 *t *,那么这两个字符串是同构的。所有出现的字符都必须用另一个字符替换,同时保留字符的顺序。两个字符不能映射到同一个字符上,但字符可以映射自己本身。
比如输入: s = "egg", t = "add",输出: true
输入: s = "foo", t = "bar"输出: false
var isIsomorphic = function(s, t) {
let i = 0
while (i < s.length) {
if (s.indexOf(s[i]) != t.indexOf(t[i])){
return false
}
i++
}
return true
};
console.log(isIsomorphic('foo','bar'));